Full Code of gonativeio/gonative-android for AI

gonative-public 250d90c9f096 cached
122 files
554.2 KB
123.6k tokens
685 symbols
1 requests
Download .txt
Showing preview only (594K chars total). Download the full file or copy to clipboard to get everything.
Repository: gonativeio/gonative-android
Branch: gonative-public
Commit: 250d90c9f096
Files: 122
Total size: 554.2 KB

Directory structure:
gitextract_20zoy71i/

├── .github/
│   └── workflows/
│       └── firebase-testing.yml
├── .gitignore
├── AppIcon
├── CHANGELOG.md
├── HeaderImage
├── NotificationIcon
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-project.txt
│   └── src/
│       ├── androidTest/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   ├── HelperClass.java
│       │   │   └── appConfig.json
│       │   └── java/
│       │       └── com/
│       │           └── gonative/
│       │               └── testFiles/
│       │                   ├── FirstTestClass.java
│       │                   └── TestMethods.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   ├── BlobDownloader.js
│       │   │   ├── GoNativeJSBridgeLibrary.js
│       │   │   ├── appConfig.json
│       │   │   ├── custom-icons.json
│       │   │   └── offline.html
│       │   ├── java/
│       │   │   └── io/
│       │   │       └── gonative/
│       │   │           └── android/
│       │   │               ├── ActionManager.java
│       │   │               ├── AppLinksActivity.java
│       │   │               ├── AudioUtils.java
│       │   │               ├── ConfigPreferences.java
│       │   │               ├── ConfigUpdater.java
│       │   │               ├── CustomHeaders.java
│       │   │               ├── DownloadService.java
│       │   │               ├── FileDownloader.java
│       │   │               ├── FileUploadIntentsCreator.kt
│       │   │               ├── FileWriterSharer.java
│       │   │               ├── GoNativeApplication.java
│       │   │               ├── GoNativeWindowManager.java
│       │   │               ├── HtmlIntercept.java
│       │   │               ├── IOUtils.java
│       │   │               ├── Installation.java
│       │   │               ├── JsCustomCodeExecutor.java
│       │   │               ├── JsResultBridge.java
│       │   │               ├── JsonMenuAdapter.java
│       │   │               ├── KeyboardManager.kt
│       │   │               ├── LoginManager.java
│       │   │               ├── MainActivity.java
│       │   │               ├── MySwipeRefreshLayout.java
│       │   │               ├── ProfilePicker.java
│       │   │               ├── RegistrationManager.java
│       │   │               ├── SegmentedController.java
│       │   │               ├── ShakeDialogFragment.java
│       │   │               ├── SplashActivity.java
│       │   │               ├── TabManager.java
│       │   │               ├── UrlInspector.java
│       │   │               ├── UrlNavigation.java
│       │   │               ├── WebViewPool.java
│       │   │               ├── WebViewPoolDisownPolicy.java
│       │   │               ├── files/
│       │   │               │   └── CapturedImageSaver.kt
│       │   │               └── widget/
│       │   │                   ├── CircleImageView.java
│       │   │                   ├── GoNativeDrawerLayout.java
│       │   │                   ├── GoNativeSwipeRefreshLayout.java
│       │   │                   ├── HandleView.kt
│       │   │                   ├── SwipeHistoryNavigationLayout.kt
│       │   │                   └── WebViewContainerView.java
│       │   └── res/
│       │       ├── anim/
│       │       │   └── fast_fade_out.xml
│       │       ├── drawable/
│       │       │   ├── bg_nav_icon.xml
│       │       │   ├── ic_baseline_arrow_back_24.xml
│       │       │   ├── ic_baseline_arrow_forward_24.xml
│       │       │   ├── ic_go_back.xml
│       │       │   ├── ic_go_forward.xml
│       │       │   ├── ic_stat_onesignal_default.xml
│       │       │   └── shape_rounded.xml
│       │       ├── layout/
│       │       │   ├── actionbar_title.xml
│       │       │   ├── activity_gonative.xml
│       │       │   ├── activity_subscriptions.xml
│       │       │   ├── button_menu.xml
│       │       │   ├── empty.xml
│       │       │   ├── menu_child_icon.xml
│       │       │   ├── menu_child_noicon.xml
│       │       │   ├── menu_group_icon.xml
│       │       │   ├── menu_group_noicon.xml
│       │       │   ├── profile_picker_dropdown.xml
│       │       │   ├── splash_screen.xml
│       │       │   ├── tab.xml
│       │       │   └── view_handle.xml
│       │       ├── menu/
│       │       │   └── topmenu.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── integers.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       ├── values-ko/
│       │       │   └── strings.xml
│       │       ├── values-large/
│       │       │   └── styles.xml
│       │       ├── values-night/
│       │       │   ├── colors.xml
│       │       │   └── styles.xml
│       │       ├── values-night-v29/
│       │       │   └── styles.xml
│       │       ├── values-sw600dp/
│       │       │   ├── attr.xml
│       │       │   └── dimens.xml
│       │       ├── values-sw720dp-land/
│       │       │   └── dimens.xml
│       │       ├── values-v21/
│       │       │   └── styles.xml
│       │       ├── values-v29/
│       │       │   └── styles.xml
│       │       └── xml/
│       │           ├── filepaths.xml
│       │           └── network_security_config.xml
│       └── normal/
│           └── java/
│               └── io/
│                   └── gonative/
│                       └── android/
│                           ├── GoNativeWebChromeClient.java
│                           ├── GoNativeWebviewClient.java
│                           ├── LeanWebView.java
│                           ├── PoolWebViewClient.java
│                           ├── WebViewSetup.java
│                           └── WebkitCookieManagerProxy.java
├── build.gradle
├── generate-app-icons.sh
├── generate-header-images.sh
├── generate-plugin-icons.sh
├── generate-theme.js
├── generate-tinted-icons.sh
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── plugins.gradle
└── settings.gradle

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/firebase-testing.yml
================================================
name: firebase-testing
on: [push, workflow_dispatch]
jobs:
  test-app:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up gcloud Cloud SDK environment
        # You may pin to the exact commit or the version.
        # uses: google-github-actions/setup-gcloud@94337306dda8180d967a56932ceb4ddcf01edae7
        uses: google-github-actions/setup-gcloud@v0.2.0
        with:
          # Version of the gcloud SDK to install. If unspecified or set to "latest", the latest available gcloud SDK version for the target platform will be installed. Example: "290.0.1".
          version: latest
          # Service account email address to use for authentication. This is required for legacy .p12 keys but can be omitted for .json keys. This is usually of the format <name>@<project-id>.iam.gserviceaccount.com.

          # Service account key to use for authentication. This should be the JSON formatted private key which can be exported from the Cloud Console. The value can be raw or base64-encoded.
          service_account_key: ${{ secrets.GCLOUD_KEY }}
          # ID of the Google Cloud project. If provided, this will configure gcloud to use this project ID by default for commands. Individual commands can still override the project using the --project flag which takes precedence.
          project_id: gn-test-firebase-test-lab
          # Export the provided credentials as Google Default Application Credentials. This will make the credentials available to later steps via the GOOGLE_APPLICATION_CREDENTIALS environment variable. Future steps that consume Default Application Credentials will automatically detect and use these credentials.
          export_default_credentials: true

      - name: Make gradlew executable
        run: chmod +x ./gradlew

      - name: Copying the appConfig file from androidTest to main
        run: cp -f ./app/src/androidTest/assets/appConfig.json ./app/src/main/assets/appConfig.json
        
      - name: Add dependencies in app/build.gradle
        run: sed -i "s/dependencies *\n*{/dependencies {\nandroidTestImplementation 'androidx.test.ext:junit:1.1.2'\nandroidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'\nandroidTestImplementation 'androidx.test.espresso:espresso-contrib:3.3.0'\nandroidTestImplementation 'androidx.test.espresso:espresso-web:3.3.0'\nandroidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'\n/" ./app/build.gradle

      - name: Add test runner in defaultConfig in app/build.gradle
        run: sed -i "s/defaultConfig *\n*{/defaultConfig {\ntestInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'/" ./app/build.gradle
      
      - name: Adding Helper method in GoNativeWebviewClient.java
        run: sed -i "s/super.onPageFinished *( *view *, *url *) *;/super.onPageFinished(view, url);\nHelperClass.newLoad++;/" ./app/src/normal/java/io/gonative/android/GoNativeWebviewClient.java
      
      - name: Copying HelperClass.java from androidTest/assets to normal/java/io/gonative/android/
        run: cp -f ./app/src/androidTest/assets/HelperClass.java ./app/src/normal/java/io/gonative/android/HelperClass.java

      - name: Build the App
        run: ./gradlew assembleDebug assembleAndroidTest
    
      - name: Testing the App
        run: gcloud firebase test android run --type instrumentation --app ./app/build/outputs/apk/normal/debug/app-normal-debug.apk --test ./app/build/outputs/apk/androidTest/normal/debug/app-normal-debug-androidTest.apk --device model=flo,version=21,locale=en,orientation=portrait --device model=hammerhead,version=23,locale=en,orientation=portrait --device model=griffin,version=24,locale=en,orientation=portrait --device model=G8142,version=25,locale=en,orientation=portrait --device model=star2qlteue,version=26,locale=en,orientation=portrait --device model=walleye,version=27,locale=en,orientation=portrait --device model=OnePlus5T,version=28,locale=en,orientation=portrait --device model=x1q,version=29,locale=en,orientation=portrait --device model=flame,version=30,locale=en,orientation=portrait --results-bucket cloud-test-gn-test-firebase-test-lab --timeout 300s


================================================
FILE: .gitignore
================================================
*~
.idea/
.gradle/
*.iml
local.properties
app/build/
build/
captures/
.DS_Store
plugins/


================================================
FILE: CHANGELOG.md
================================================
# Changelog

## 2014-01-04

- Fix a crash on reload with no page loaded.

## 2015-01-02

- Update to latest gradle and build tools versions, making the project compatible with Android Studio 1.0.
- Fix bugs related to syncing of tabs with sidebar menu.

## 2014-12-23

- Allow setting of viewport while preserving ability to zoom.
- Allow dynamic config of navigation title image URLs.
- Various bug fixes involving javascript after page load, and tab coloring, tab animations, and a crash on application resume.

## 2014-12-22

- Fix various threading bugs where UI methods were called from non-UI threads.

## 2014-12-05

- Support showing the navigation title image on specific URLs.

## 2014-12-03

- Support customizing user agent per URL.
- Add color styling options for tabs.

## 2014-11-30

- New tabs with better material design and animations.
- Fix some automatic icon generation scripts.

## 2014-11-26

- Fix a crash involving webview pools.

## 2014-11-25

- Add support for custom actions in action bar.


================================================
FILE: README.md
================================================
# Archive Notice

This repository is archived and is no longer being maintained. You can now build an app using our online App Studio at https://median.co/app and download custom iOS source code for free.


gonative-android
================

This is the native Android code previously used by [GoNative](https://median.co).

It allows the creation of apps from existing mobile-optimized websites.

How to use
------------
Import into Android Studio. Edit appConfig.json as appropriate.


================================================
FILE: app/.gitignore
================================================
build/


================================================
FILE: app/build.gradle
================================================
import groovy.json.JsonSlurper

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
//[enabled by builder] apply plugin: 'com.google.gms.google-services'
//[enabled by builder] apply plugin: 'com.google.firebase.crashlytics'

ext {
    fbAppId = ""
    fbClientToken = ""
    onesignalAppId = ""
    adMobAppId = ""
    googleServiceInvalid = "false"
    auth0Domain = ""
    auth0Scheme = ""
}

task parseAppConfig {
    def jsonFile = file('src/main/assets/appConfig.json')
    def parsedJson = new JsonSlurper().parseText(jsonFile.text)
    if (parsedJson.services.facebook) {
        if (parsedJson.services.facebook.appId) {
            fbAppId = parsedJson.services.facebook.appId
        }
        if (parsedJson.services.facebook.clientToken) {
            fbClientToken = parsedJson.services.facebook.clientToken
        }
    }
    if (parsedJson.services.socialLogin && parsedJson.services.socialLogin.facebookLogin) {
        if (parsedJson.services.socialLogin.facebookLogin.appId) {
            fbAppId = parsedJson.services.socialLogin.facebookLogin.appId
        }
        if (parsedJson.services.socialLogin.facebookLogin.clientToken) {
            fbClientToken = parsedJson.services.socialLogin.facebookLogin.clientToken
        }
    }
    if (parsedJson.services.oneSignal && parsedJson.services.oneSignal.applicationId) {
        onesignalAppId = parsedJson.services.oneSignal.applicationId
    }
    if (parsedJson.services.admob && parsedJson.services.admob.admobAndroid && parsedJson.services.admob.admobAndroid.applicationId) {
        adMobAppId = parsedJson.services.admob.admobAndroid.applicationId
    }
    if (parsedJson.services.braze) {
        if (parsedJson.services.braze.androidApiKey) {
            gradle.ext.set("braze_api_key", parsedJson.services.braze.androidApiKey)
        }
        if (parsedJson.services.braze.androidEndpointKey) {
            gradle.ext.set("braze_endpoint_key", parsedJson.services.braze.androidEndpointKey)
        }
    }
    if (parsedJson.services.auth0) {
        if (parsedJson.services.auth0.domain) {
            auth0Domain = parsedJson.services.auth0.domain
        }
        if (parsedJson.services.auth0.scheme) {
            auth0Scheme = parsedJson.services.auth0.scheme
        }
    }
}

task checkGoogleService {
    plugins.withId("com.google.gms.google-services") {
        def googleServiceJsonFile = file('google-services.json')
        if (project.file(googleServiceJsonFile).exists()) {
            if (googleServiceJsonFile.text.isEmpty()) {
                googleServiceInvalid = "true"
            }
        } else {
            googleServiceInvalid = "true"
        }
    }
}

build.dependsOn parseAppConfig
build.dependsOn checkGoogleService

android {
    defaultConfig {
        compileSdk 33
        minSdkVersion 21
        targetSdkVersion 33
        applicationId "io.gonative.android"
        versionCode 1
        multiDexEnabled true
        vectorDrawables.useSupportLibrary = true

        manifestPlaceholders = [manifestApplicationId: "${applicationId}",
                                onesignal_app_id: onesignalAppId,
                                onesignal_google_project_number: "",
                                admob_app_id: adMobAppId,
                                facebook_app_id: fbAppId,
                                facebook_client_token: fbClientToken,
                                auth0Domain: auth0Domain, auth0Scheme: auth0Scheme ]
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    signingConfigs {
        release {
            storeFile file("../../release.keystore")
            storePassword "password"
            keyAlias "release"
            keyPassword "password"
        }
        upload {
            storeFile file("../../upload.keystore")
            storePassword "password"
            keyAlias "upload"
            keyPassword "password"
        }
    }

    buildTypes {
        debug {
	        applicationIdSuffix ".debug"
        }
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
            zipAlignEnabled true
            debuggable project.getProperties().get("enableLogsInRelease").toBoolean()
            signingConfig signingConfigs.release
        }
        upload {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'
            zipAlignEnabled true
            matchingFallbacks = ['release']
            debuggable project.getProperties().get("enableLogsInRelease").toBoolean()
            signingConfig signingConfigs.upload
        }
        buildTypes.each {
            it.buildConfigField 'boolean', 'GOOGLE_SERVICE_INVALID', googleServiceInvalid
        }
    }

    flavorDimensions "webview"

    productFlavors {
        normal {
            dimension "webview"
        }
    }
    namespace 'io.gonative.android'
    testNamespace '${applicationId}.test'
}

dependencies {
    /**** dependencies used by all apps ****/
    implementation "androidx.core:core-ktx:1.10.1"
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.aurelhubert:ahbottomnavigation:2.3.4'
    implementation 'com.squareup:seismic:1.0.2'
    implementation 'androidx.webkit:webkit:1.7.0'
    implementation 'androidx.core:core-splashscreen:1.0.1'
    implementation "com.github.gonativeio:gonative-icons:$iconsVersion"
    implementation "com.github.gonativeio:gonative-android-core:$coreVersion"
    /**** end all apps ****/

    /**** add-on module dependencies ****/
    /**** end modules ****/
    
    /**** Google Android and Play Services dependencies ****/
    implementation 'androidx.multidex:multidex:2.0.1'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'androidx.browser:browser:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.9.0'
    implementation "androidx.drawerlayout:drawerlayout:1.2.0"
    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
    /**** end google ****/

    /**** local dependencies ****/
    implementation fileTree(dir: 'libs', include: '*.jar')
    implementation fileTree(dir: 'libs', include: '*.aar')
    /**** end local ****/
}

apply from: file("../plugins.gradle"); applyNativeModulesAppBuildGradle(project)

================================================
FILE: app/proguard-project.txt
================================================
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# Add any project specific keep options here:

# 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 *;
#}

# webview interfaces

-keepclassmembers class io.gonative.android.ProfilePicker$ProfileJsBridge {
    <methods>;
}
-keepclassmembers class io.gonative.android.MainActivity$StatusCheckerBridge {
    <methods>;
}
-keepattributes JavascriptInterface

# stuff for google play services
-keep class * extends java.util.ListResourceBundle {
    protected Object[][] getContents();
}

-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {
    public static final *** NULL;
}

-keepnames @com.google.android.gms.common.annotation.KeepName class *
-keepclassmembernames class * {
    @com.google.android.gms.common.annotation.KeepName *;
}

-keepnames class * implements android.os.Parcelable {
    public static final ** CREATOR;
}

# Google Cloud Messaging
-keep class com.google.android.gms.** { *; }
-keep interface com.google.android.gms.** { *; }

# appcompat library
-dontwarn android.support.v4.**
-keep class android.support.v4.** { *; }
-keep interface android.support.v4.** { *; }
-dontwarn android.support.v7.**
-keep class android.support.v7.** { *; }
-keep interface android.support.v7.** { *; }


================================================
FILE: app/src/androidTest/AndroidManifest.xml
================================================
<!-- Ignores minSdk 18 requirement error during build -->
<manifest xmlns:tools="http://schemas.android.com/tools">
    <uses-sdk tools:overrideLibrary="android_libs.ub_uiautomator"/>
</manifest>

================================================
FILE: app/src/androidTest/assets/HelperClass.java
================================================
package io.gonative.android;

public class HelperClass {
    public volatile static int newLoad = 0;
}


================================================
FILE: app/src/androidTest/assets/appConfig.json
================================================
{
  "general": {
    "userAgentAdd": "gonative",
    "initialUrl": "https://gonative.io",
    "appName": "GoNative.io"
  },
  "navigation": {
    "androidPullToRefresh": true,
    "sidebarNavigation": {
      "sidebarEnabledRegex": null,
      "menus": [{
        "name": "default",
        "items": [{
          "url": "https://gonative.io",
          "label": "Home",
          "subLinks": []
        }, {
          "url": "https://gonative.io/about",
          "label": "About",
          "subLinks": []
        }, {
          "url": "https://gonative.io/examples",
          "label": "Examples",
          "subLinks": []
        }],
        "active": true
      }]
    },
    "tabNavigation": {
      "tabSelectionConfig": [{
        "id": "1",
        "regex": ".*about.*"
      }],
      "tabMenus": [{
        "id": "1",
        "items": [{
          "icon": "fa-cloud",
          "label": "Tab 1",
          "url": "https://www.gonative.io/pricing"
        }, {
          "icon": "fa-globe",
          "label": "Tab 2",
          "url": "https://www.gonative.io/examples"
        }, {
          "icon": "fa-users",
          "label": "Tab 3",
          "url": "javascript:alert('You selected tab 3. These tabs are only shown on the about page')"
        }]
      }],
      "active": true
    },
    "actionConfig": {
      "active": true,
      "actions": [{
        "id": "exampleActions",
        "items": [{
          "label": "Globe",
          "icon": "fa-globe",
          "url": "javascript:alert('You tapped the globe! It only appears on the Examples page')"
        }]
      }],
      "actionSelection": [{
        "regex": ".*/examples.*",
        "id": "exampleActions"
      }]
    },
    "regexInternalExternal": {
      "rules": [{
        "regex": "https?://([-\\w]+\\.)*facebook\\.com/login.php.*",
        "internal": true
      }, {
        "regex": "https?://([-\\w]+\\.)*facebook\\.com/pages/.*",
        "internal": false
      }, {
        "regex": "https?://([-\\w]+\\.)*facebook\\.com/sharer\\.php.*",
        "internal": false
      }, {
        "regex": "https?://([-\\w]+\\.)*plus\\.google\\.com/share.*",
        "internal": false
      }, {
        "regex": "https?://([-\\w]+\\.)*twitter\\.com/intent/.*",
        "internal": false
      }, {
        "regex": "https?://([-\\w]+\\.)*gonative\\.io/?.*",
        "internal": true
      }, {
        "regex": "https?://([-\\w]+\\.)*google\\.com/?.*",
        "internal": true
      }, {
        "regex": "https://gonative-test-web.web.app/.*",
        "internal": true
      },
        {
          "regex": "https://us-central1-gn-test-firebase-test-lab.cloudfunctions.net/.*",
          "internal": true
        }],
      "active": true
    },
    "redirects": [{
      "from": "https://example.com/from/",
      "to": "https://example.com/to/"
    }]
  },
  "forms": {
    "search": {
      "active": true,
      "searchTemplateURL": "https://us-central1-gn-test-firebase-test-lab.cloudfunctions.net/gnTestSearch?q="
    }
  },
  "styling": {
    "showActionBar": true,
    "showNavigationBar": true,
    "iosTitleColor": "#333333",
    "iosTintColor": "#0091fe",
    "androidTheme": "Light.DarkActionBar",
    "androidSidebarBackgroundColor": "#111111",
    "androidSidebarForegroundColor": "#d0d0d0",
    "androidHideTitleInActionBar": false,
    "androidPullToRefreshColor": "#333333",
    "androidTabBarBackgroundColor": "#fefefe",
    "androidTabBarTextColor": "#747474",
    "androidTabBarIndicatorColorx": "#2f79fe",
    "androidShowSplash": true,
    "androidShowSplashMaxTime": null,
    "androidShowSplashForceTime": null,
    "disableAnimations": false,
    "menuAnimationDuration": 0.15,
    "transitionInteractiveDelayMax": 0.2
  },
  "permissions": {
    "usesGeolocation": false,
    "androidDownloadToPublicStorage": false
  },
  "services": {
    "oneSignal": {
      "active": false,
      "applicationId": ""
    },
    "facebook": {
      "active": false,
      "appId": "",
      "displayName": ""
    },
    "registration": {
      "active": false,
      "endpoints": [{
        "url": "https://gonative.io/example_push_endpoint",
        "dataType": "onesignal",
        "urlRegex": ".*/loginfinished"
      }]
    }
  }
}


================================================
FILE: app/src/androidTest/java/com/gonative/testFiles/FirstTestClass.java
================================================
package com.gonative.testFiles;

import android.webkit.WebView;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
import androidx.test.uiautomator.UiDevice;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import io.gonative.android.MainActivity;
import io.gonative.android.R;
import io.gonative.gonative_core.AppConfig;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;

@RunWith(AndroidJUnit4.class)
@SdkSuppress(minSdkVersion = 18)
@LargeTest
public class FirstTestClass{
    TestMethods testMethods;
    AppConfig appConfig;
    WebView webView;
    private UiDevice uiDevice;

    @Rule
    public ActivityScenarioRule<MainActivity> activityScenarioRule = new ActivityScenarioRule<>(MainActivity.class);

    @Before
    public void initMethod() throws InterruptedException {
        for(int i = 0; i < 10; i++){
            try{
                uiDevice = UiDevice.getInstance(getInstrumentation());
            }catch (RuntimeException runtimeException){
                Thread.sleep(2000);
                continue;
            }
            Thread.sleep(1000);
            break;
        }
        activityScenarioRule.getScenario().onActivity(activity -> {
            appConfig = AppConfig.getInstance(activity);
            webView = activity.findViewById(R.id.webview);
            testMethods = new TestMethods(activity, webView);
        });
    }

    //Sidebar Navigation Test
    @Test
    public void testSidebarNavigation() throws InterruptedException, JSONException {
        if(appConfig.showNavigationMenu && (appConfig.menus.get("default") != null)){
            if(appConfig.menus.get("default") == null) throw new RuntimeException("Navigation drawer list not found.");
            else {
                testMethods.waitForPageLoaded();
                testMethods.testNavigation(appConfig.menus.get("default"));
            }
        }
    }

    //Tab Menu Navigation Test
    @Test
    public void testTabMenuNavigation() throws JSONException, InterruptedException {
        if(appConfig.tabMenuRegexes.size() == 0) throw new RuntimeException("No Tab Menus found.");
        else{
            testMethods.waitForPageLoaded();
            testMethods.m_testTabNavigation(appConfig.tabMenus, appConfig.tabMenuRegexes);
        }
    }

    //Internal vs External Links Test
    @Test
    public void testIvE() throws InterruptedException {
        testMethods.waitForPageLoaded();
        testMethods.testInternalvExternalLinks(uiDevice);
    }

    //Pull to Refresh Test
    @Test
    public void pullToRefresh() throws InterruptedException {
        if(appConfig.pullToRefresh){
            testMethods.waitForPageLoaded();
            testMethods.testPullToRefresh();
        }
    }

    //Search Button Test
    @Test
    public void testSearch() throws InterruptedException {
        if(appConfig.searchTemplateUrl != null && !appConfig.searchTemplateUrl.isEmpty()){
            testMethods.waitForPageLoaded();
            testMethods.testSearchButton();
        }
    }

    //Refresh Button Test
    @Test
    public void testRefreshButton() throws InterruptedException {
        if(appConfig.showRefreshButton){
            testMethods.waitForPageLoaded();
            testMethods.testRefreshButton();
        }
    }
}


================================================
FILE: app/src/androidTest/java/com/gonative/testFiles/TestMethods.java
================================================
package com.gonative.testFiles;
import android.webkit.WebView;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.espresso.PerformException;
import androidx.test.espresso.contrib.DrawerActions;
import androidx.test.espresso.web.webdriver.Locator;
import androidx.test.uiautomator.UiDevice;
import io.gonative.android.HelperClass;
import org.json.JSONArray;
import org.json.JSONException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;
import static androidx.test.espresso.Espresso.onData;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.Espresso.pressBackUnconditionally;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.action.ViewActions.pressImeActionButton;
import static androidx.test.espresso.action.ViewActions.swipeDown;
import static androidx.test.espresso.action.ViewActions.typeText;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import static androidx.test.espresso.matcher.ViewMatchers.withResourceName;
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;
import static androidx.test.espresso.web.sugar.Web.onWebView;
import static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;
import static androidx.test.espresso.web.webdriver.DriverAtoms.getText;
import static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.equalTo;
import io.gonative.android.MainActivity;
import io.gonative.android.R;

public class TestMethods {

    public TestMethods(MainActivity mainActivity, WebView webView){
        m_mainActivity = mainActivity;
        m_webView = webView;
    }

    protected static String currentURL = "URL Not assigned yet";
    protected static String prevURL = "URL Not assigned yet";
    private final WebView m_webView;
    private final MainActivity m_mainActivity;

    private void m_UpdateCurrentURL() throws InterruptedException {
        m_mainActivity.runOnUiThread(() -> currentURL = m_webView.getOriginalUrl());
        Thread.sleep(1000);
    }

    protected int getNewLoad(){
        return HelperClass.newLoad;
    }

    public boolean isURL(String url){
        try {
            new URL(url);
            return true;
        }catch (MalformedURLException e){
            return false;
        }
    }

    public void waitForPageLoaded() throws InterruptedException {
        int counter = 0;
        while (getNewLoad() == 0) {
            if (counter >= 15) throw new RuntimeException("Page failed to load in less than 15 seconds.");
            Thread.sleep(1000);
            counter++;
        }
        m_UpdateCurrentURL();
        Thread.sleep(1000);
        counter = 0;
        while(currentURL == null && counter <= 10){
            Thread.sleep(1000);
            counter++;
            m_UpdateCurrentURL();
        }
        if(currentURL != null) HelperClass.newLoad = 0;
        else throw new RuntimeException("Current URL cannot be retrieved from the WebView.");
    }

    public void testNavigation(JSONArray sidebarObjects) throws InterruptedException, JSONException {
        for(int i = 0; i < sidebarObjects.length(); i++){
            onView(withId(R.id.drawer_layout)).perform(DrawerActions.open());
            onData(anything()).inAdapterView(withId(R.id.drawer_list)).atPosition(i).perform(click());
            waitForPageLoaded();
            String sidebarURL = sidebarObjects.getJSONObject(i).getString("url");
            if(!currentURL.matches(sidebarURL)) throw new RuntimeException("Sidebar Menu " + (i+1) + " did not load the designated URL - " + sidebarURL);
        }
    }

    public void testInternalvExternalLinks(UiDevice uiDevice) throws InterruptedException {
        m_mainActivity.runOnUiThread(() -> m_webView.loadUrl("https://gonative-test-web.web.app/"));
        waitForPageLoaded();
        onWebView().withElement(findElement(Locator.ID, "facebook_link")).perform(webClick());
        waitForPageLoaded();
        String dFacebook = uiDevice.getCurrentPackageName();

        if(dFacebook.contains("io.gonative.android")){
            uiDevice.pressBack();
            Thread.sleep(2000);
        }else throw new RuntimeException("Facebook link opened externally");

        onWebView().withElement(findElement(Locator.ID, "twitter_link")).perform(webClick());
        Thread.sleep(8000);
        String dTwitter = uiDevice.getCurrentPackageName();
        if(dTwitter.contains("io.gonative.android")) throw new RuntimeException("Twitter opened internally.");
        else {
            uiDevice.pressBack();
            Thread.sleep(1000);
        }
    }

    public void testRefreshButton() throws InterruptedException {
        onView(withId(R.id.action_refresh)).perform(click());
        waitForPageLoaded();
    }

    public void testSearchButton() throws InterruptedException {
        String query = "Gonative";
        onView(withId(R.id.action_search)).perform(click());
        Thread.sleep(1000);
        onView(withResourceName("search_src_text")).perform(typeText(query), pressImeActionButton());
        waitForPageLoaded();
        try{
            onWebView().withElement(findElement(Locator.ID, "search_param")).check(webMatches(getText(), equalTo(query)));
        }catch (Exception exception){
            throw new RuntimeException("Search button failed to load the results with query - " + query);
        }
        pressBackUnconditionally();
        Thread.sleep(2000);
    }

    public void m_testTabNavigation(HashMap<String, JSONArray> tabMenus, ArrayList<Pattern> tabMenuRegexes) throws JSONException, InterruptedException {
        while(!(currentURL.matches(tabMenuRegexes.get(0).pattern()))){
            m_mainActivity.runOnUiThread(() -> m_webView.loadUrl("https://gonative.io/about/"));
            waitForPageLoaded();
        }
        if (tabMenus.size() == 0) throw new RuntimeException("No Tab Menus Added.");
        else {
            for (Pattern p : tabMenuRegexes) {
                if (currentURL.matches(p.pattern())) {
                    try {
                        for (String i : tabMenus.keySet()) {
                            for (int j = 0; j < tabMenus.get(i).length(); ) {
                                if (currentURL.matches(p.pattern())) {
                                    if (isURL(tabMenus.get(i).getJSONObject(j).get("url").toString())) {
                                        Thread.sleep(1000);
                                        onView(withText(tabMenus.get(i).getJSONObject(j).get("label").toString())).perform(click());
                                        waitForPageLoaded();
                                        prevURL = currentURL;
                                        pressBackUnconditionally();
                                        waitForPageLoaded();
                                        String tabURL = tabMenus.get(i).getJSONObject(j).get("url").toString();
                                        if (!(prevURL.matches(tabURL))) throw new RuntimeException("Tab " + (j+1) + " could not load the designated URL - " + prevURL);
                                        j++;
                                        prevURL = currentURL;
                                    } else {
                                        onView(withText(tabMenus.get(i).getJSONObject(j).get("label").toString())).perform(click());
                                        j++;
                                        Thread.sleep(2000);
                                    }
                                }
                            }
                        }
                    } catch (NoMatchingViewException | PerformException noMatchingViewException) {
                        throw new RuntimeException("Tab Menu not displayed in the desired regex: " + p.pattern());
                    }
                } else{
                    throw new RuntimeException("No Tab Menus found on the current page.");
                }
            }
        }
    }

    public void testPullToRefresh() throws InterruptedException {
        onView(withId(R.id.webview)).perform(swipeDown());
        waitForPageLoaded();
    }
}


================================================
FILE: app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:versionName="1.0.0">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <!-- <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> -->
    <!-- <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission
        android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

    <!-- Camera permissions -->
    <!--<uses-permission android:name="android.permission.CAMERA"/>-->
    <!--<uses-feature android:name="android.hardware.camera" android:required="false" />-->

    <!-- Microphone permissions -->
    <!--<uses-permission android:name="android.permission.RECORD_AUDIO"/>-->
    <!--<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>-->

    <!-- Bluetooth permissions -->
    <!--<uses-permission android:name="android.permission.BLUETOOTH" />-->
    <!--<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />-->

    <!-- permissions for push messages -->
    <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
    <permission
        android:name="${applicationId}.permission.C2D_MESSAGE"
        android:protectionLevel="signature" />
    <uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />

    <!-- permissions to block phone calls -->
    <!--<uses-permission android:name="android.permission.READ_CONTACTS" />-->
    <!--<uses-permission android:name="android.permission.READ_CALL_LOG" />-->
    <!--<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />-->

    <queries>
        <!-- Camera -->
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
        <intent>
            <action android:name="android.media.action.VIDEO_CAPTURE" />
        </intent>

        <!-- Gallery -->
        <intent>
            <action android:name="android.intent.action.GET_CONTENT" />
            <data android:mimeType="image/*" />
        </intent>
        <intent>
            <action android:name="android.intent.action.PICK" />
            <data android:mimeType="image/*" />
        </intent>
        <intent>
            <action android:name="android.intent.action.CHOOSER" />
        </intent>
    </queries>

    <application
        android:name=".GoNativeApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:logo="@drawable/ic_actionbar"
        android:networkSecurityConfig="@xml/network_security_config"
        android:requestLegacyExternalStorage="true"
        android:supportsRtl="true"
        android:theme="@style/GoNativeTheme.NoActionBar">

        <activity
            android:name="io.gonative.android.MainActivity"
            android:configChanges="orientation|screenSize"
            android:windowSoftInputMode="adjustResize"
            android:exported="true"
            android:label="@string/app_name"
            tools:node="merge" />

        <activity
            android:name=".AppLinksActivity"
            android:exported="true"
            android:launchMode="singleTask">
            <!--additional intent filters-->

            <!--example: -->
            <!--<intent-filter>-->
            <!--<action android:name="android.intent.action.VIEW"></action>-->
            <!--<category android:name="android.intent.category.DEFAULT"></category>-->
            <!--<category android:name="android.intent.category.BROWSABLE"></category>-->
            <!--<data android:scheme="http"></data>-->
            <!--<data android:scheme="https"></data>-->
            <!--<data android:host="gonative.io"></data>-->
            <!--<data android:pathPrefix="/"></data>-->
            <!--</intent-filter>-->
        </activity>

        <!-- For file sharing without having to use external permissions. -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/filepaths" />
        </provider>

        <!-- Facebook -->
        <meta-data
            android:name="com.facebook.sdk.ApplicationId"
            android:value="fb${facebook_app_id}" />
        <meta-data
            android:name="com.facebook.sdk.ClientToken"
            android:value="${facebook_client_token}" />

        <activity
            android:name=".SplashActivity"
            android:exported="true"
            android:theme="@style/SplashTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service android:name=".DownloadService"/>

    </application>

</manifest>


================================================
FILE: app/src/main/assets/BlobDownloader.js
================================================
// This is used because download from native side won't have session changes.

function gonativeDownloadBlobUrl(url) {
	var req = new XMLHttpRequest();
	req.open('GET', url, true);
	req.responseType = 'blob';

	req.onload = function(event) {
		var blob = req.response;
		saveBlob(blob);
	};
	req.send();

	function sendMessage(message) {
	    if (window.webkit && window.webkit.messageHandlers &&
	        window.webkit.messageHandlers.fileWriterSharer) {
	        window.webkit.messageHandlers.fileWriterSharer.postMessage(message);
	    }
	    if (window.gonative_file_writer_sharer && window.gonative_file_writer_sharer.postMessage) {
			window.gonative_file_writer_sharer.postMessage(JSON.stringify(message));
	    }
	}

	function saveBlob(blob, filename) {
	    var chunkSize = 1024 * 1024; // 1mb
	    var index = 0;
	    // random string to identify this file transfer
	    var id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);

	    function sendHeader() {
	        sendMessage({
	            event: 'fileStart',
	            id: id,
	            size: blob.size,
	            type: blob.type,
	            name: filename
	        });
	    }

	    function sendChunk() {
	        if (index >= blob.size) {
	            return sendEnd();
	        }

	        var chunkToSend = blob.slice(index, index + chunkSize);
	        var reader = new FileReader();
	        reader.readAsDataURL(chunkToSend);
	        reader.onloadend = function() {
	            sendMessage({
	                event: 'fileChunk',
	                id: id,
	                data: reader.result
	            });
	            index += chunkSize;
	            setTimeout(sendChunk);
	        };
	    }
	    
	    function sendEnd() {
	        sendMessage({
	            event: 'fileEnd',
	            id:id
	        });
	    }
	    
	    sendHeader();
	    gonative_run_after_storage_permissions.push(sendChunk);
	}
}

gonative_run_after_storage_permissions = [];
function gonativeGotStoragePermissions() {
    while (gonative_run_after_storage_permissions.length > 0) {
        var run = gonative_run_after_storage_permissions.shift();
        run();
    }
}


================================================
FILE: app/src/main/assets/GoNativeJSBridgeLibrary.js
================================================
// this function accepts a callback function as params.callback that will be called with the command results
// if a callback is not provided it returns a promise that will resolve with the command results
function addCommandCallback(command, params, persistCallback) {
    if(params && (params.callback || params.callbackFunction)){
        // execute command with provided callback function
        addCommand(command, params, persistCallback);
    } else {
        // create a temporary function and return a promise that executes command
        var tempFunctionName = '_gonative_temp_' + Math.random().toString(36).slice(2);
        if(!params) params = {};
        params.callback = tempFunctionName;
        return new Promise(function(resolve, reject) {
            // declare a temporary function
            window[tempFunctionName] = function(data) {
                resolve(data);
                delete window[tempFunctionName];
            }
            // execute command
            addCommand(command, params);
        });
    }
}

function addCallbackFunction(callbackFunction, persistCallback){
    var callbackName;
    if(typeof callbackFunction === 'string'){
        callbackName = callbackFunction;
    } else {
        callbackName = '_gonative_temp_' + Math.random().toString(36).slice(2);
        window[callbackName] = function(...args) {
            callbackFunction.apply(null, args);
            if(!persistCallback){ // if callback is used just once
                delete window[callbackName];
            }
        }
    }
    return callbackName;
}

function addCommand(command, params, persistCallback){
    var data = undefined;
    if(params) {
        var commandObject = {};
        if(params.callback && typeof params.callback === 'function'){
            params.callback = addCallbackFunction(params.callback, persistCallback);
        }
        if(params.callbackFunction && typeof params.callbackFunction === 'function'){
            params.callbackFunction = addCallbackFunction(params.callbackFunction, persistCallback);
        }
        if(params.statuscallback && typeof params.statuscallback === 'function'){
            params.statuscallback = addCallbackFunction(params.statuscallback, persistCallback);
        }
        commandObject.gonativeCommand = command;
        commandObject.data = params;
        data = JSON.stringify(commandObject);
    } else data = command;

    JSBridge.postMessage(data);
}

///////////////////////////////
////    General Commands   ////
///////////////////////////////

var gonative = {};

// to be modified as required
gonative.nativebridge = {
    custom: function (params){
        addCommand("gonative://nativebridge/custom", params);
    },
    multi: function (params){
        addCommand("gonative://nativebridge/multi", params);
    }
};

gonative.registration = {
    send: function(customData){
        var params = {customData: customData};
        addCommand("gonative://registration/send", params);
    }
};

gonative.sidebar = {
    setItems: function (params){
        addCommand("gonative://sidebar/setItems", params);
    },
    getItems: function (params){
        return addCommandCallback("gonative://sidebar/getItems", params);
    }
};

gonative.tabNavigation = {
    selectTab: function (tabIndex){
        addCommand("gonative://tabs/select/" + tabIndex);
    },
    deselect: function (){
        addCommand("gonative://tabs/deselect");
    },
    setTabs: function (tabsObject){
        var params = {tabs: tabsObject};
        addCommand("gonative://tabs/setTabs", params);
    }
};

gonative.share = {
    sharePage: function (params){
        addCommand("gonative://share/sharePage", params);
    },
    downloadFile: function (params){
        addCommand("gonative://share/downloadFile", params);
    },
    downloadImage: function(params){
        addCommand("gonative://share/downloadImage", params);
    }
};

gonative.open = {
    appSettings: function (){
        addCommand("gonative://open/app-settings");
    }
};

gonative.webview = {
    clearCache: function(){
        addCommand("gonative://webview/clearCache");
    },
    clearCookies: function(){
        addCommand("gonative://webview/clearCookies");
    },
    reload: function (){
        addCommand("gonative://webview/reload");
    }
};

gonative.config = {
    set: function(params){
        addCommand("gonative://config/set", params);
    }
};

gonative.navigationTitles = {
    set: function (parameters){
        var params = {
            persist: parameters.persist,
            data: parameters
        };
        addCommand("gonative://navigationTitles/set", params);
    },
    setCurrent: function (params){
        addCommand("gonative://navigationTitles/setCurrent", params);
    },
    revert: function(){
        addCommand("gonative://navigationTitles/set?persist=true");
    }
};

gonative.navigationLevels = {
    set: function (parameters){
        var params = {
            persist: parameters.persist,
            data: parameters
        };
        addCommand("gonative://navigationLevels/set", params);
    },
    setCurrent: function(params){
        addCommand("gonative://navigationLevels/set", params);
    },
    revert: function(){
        addCommand("gonative://navigationLevels/set?persist=true");
    }
};

gonative.statusbar = {
    set: function (params){
        addCommand("gonative://statusbar/set", params);
    }
};

gonative.screen = {
    setBrightness: function(data){
        var params = data;
        if(typeof params === 'number'){
            params = {brightness: data};
        }
        addCommand("gonative://screen/setBrightness", params);
    },
    setMode: function(params) {
        if (params.mode) {
            addCommand("gonative://screen/setMode", params);
        }
    }
};

gonative.navigationMaxWindows = {
    set: function (maxWindows, autoClose){
        var params = {
            data: maxWindows,
            autoClose: autoClose,
            persist: true
        };
        addCommand("gonative://navigationMaxWindows/set", params);
    },
    setCurrent: function(maxWindows, autoClose){
        var params = {data: maxWindows, autoClose: autoClose};
        addCommand("gonative://navigationMaxWindows/set", params);
    }
}

gonative.window = {
    open: function (urlString) {
        var params = {url: urlString};
        addCommand("gonative://window/open", params);
    },
    close: function () {
        addCommand("gonative://window/close");
    }
}

gonative.connectivity = {
    get: function (params){
        return addCommandCallback("gonative://connectivity/get", params);
    },
    subscribe: function (params){
        return addCommandCallback("gonative://connectivity/subscribe", params, true);
    },
    unsubscribe: function (){
        addCommand("gonative://connectivity/unsubscribe");
    }
};

gonative.run = {
    deviceInfo: function(){
        addCommand("gonative://run/gonative_device_info");
    }
};

gonative.deviceInfo = function(params){
    return addCommandCallback("gonative://run/gonative_device_info", params, true);
};

gonative.internalExternal = {
    set: function(params){
        addCommand("gonative://internalExternal/set", params);
    }
};

gonative.clipboard = {
    set: function(params){
        addCommand("gonative://clipboard/set", params);
    },
    get: function(params){
        return addCommandCallback("gonative://clipboard/get", params);
    }
};

gonative.keyboard = {
    info: function(params){
        return addCommandCallback("gonative://keyboard/info", params);
    },
    listen: function(callback){
        var params = {callback};
        addCommand("gonative://keyboard/listen", params);
    }
};

//////////////////////////////////////
////   Webpage Helper Functions   ////
//////////////////////////////////////

function gonative_match_statusbar_to_body_background_color() {
    let rgb = window.getComputedStyle(document.body, null).getPropertyValue('background-color');
    let sep = rgb.indexOf(",") > -1 ? "," : " ";
    rgb = rgb.substring(rgb.indexOf('(')+1).split(")")[0].split(sep).map(function(x) { return x * 1; });
    if(rgb.length === 4){
        rgb = rgb.map(function(x){ return parseInt(x * rgb[3]); })
    }
    let hex = '#' + rgb[0].toString(16).padStart(2,'0') + rgb[1].toString(16).padStart(2,'0') + rgb[2].toString(16).padStart(2,'0');
    let luma = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; // per ITU-R BT.709
    if(luma > 40){
        gonative.statusbar.set({'style': 'dark', 'color': hex});
    }
    else{
        gonative.statusbar.set({'style': 'light', 'color': hex});
    }
}

///////////////////////////////
////   Android Exclusive   ////
///////////////////////////////

gonative.android = {};

gonative.android.geoLocation = {
    promptLocationServices: function(){
        addCommand("gonative://geoLocation/promptLocationServices");
    },
    isLocationServicesEnabled: function(params) {
        return addCommandCallback("gonative://geoLocation/isLocationServicesEnabled", params);
    }
};

gonative.android.screen = {
    fullscreen: function(){
        addCommand("gonative://screen/fullscreen");
    },
    normal: function(){
        addCommand("gonative://screen/normal");
    },
    keepScreenOn: function(){
        addCommand("gonative://screen/keepScreenOn");
    },
    keepScreenNormal: function(){
        addCommand("gonative://screen/keepScreenNormal");
    }
};

gonative.android.audio = {
    requestFocus: function(enabled){
        var params = {enabled: enabled};
        addCommand("gonative://audio/requestFocus", params);
    }
};

gonative.android.swipeGestures = {
    enable: function() {
        addCommand("gonative://swipeGestures/enable");
    },
    disable: function() {
        addCommand("gonative://swipeGestures/disable");
    }
}


================================================
FILE: app/src/main/assets/appConfig.json
================================================
{
  "general": {
    "appName": "GoNative.io",
    "initialUrl": "https://gonative.io/",
    "userAgentRegexes": [],
    "replaceStrings": [],
    "nativeBridgeUrls": [],
    "languages": [],
    "userAgentAdd": "gonative",
    "enableWindowOpen": true,
    "forceScreenOrientation": null
  },
  "navigation": {
    "tabNavigation": {
      "tabSelectionConfig": [
        {
          "id": "1",
          "regex": "https://gonative.io/"
        }
      ],
      "tabMenus": [
        {
          "items": [
            {
              "subLinks": [],
              "icon": "custom custom-gonative-icon",
              "label": "Home",
              "url": "https://gonative.io"
            },
            {
              "subLinks": [],
              "icon": "fas fa-globe",
              "label": "Tab 2",
              "url": "javascript:alert('You selected tab 2. These tabs are only shown on the home page')"
            },
            {
              "subLinks": [],
              "icon": "fas fa-laptop-code",
              "label": "GoNative Demo",
              "url": "https://gonative.io/demo"
            }
          ],
          "id": "1"
        }
      ],
      "active": true
    },
    "sidebarNavigation": {
      "menuSelectionConfig": {
        "testURL": null,
        "redirectLocations": [
          {
            "regex": null,
            "menuName": "default",
            "loggedIn": false
          },
          {
            "regex": ".*",
            "menuName": "default",
            "loggedIn": true
          }
        ]
      },
      "menus": [
        {
          "items": [
            {
              "subLinks": [],
              "label": "Sample Home",
              "url": "https://gonative.io",
              "icon": "fas fa-home"
            },
            {
              "subLinks": [],
              "label": "Sample About",
              "url": "https://gonative.io/about",
              "icon": "fas fa-user"
            }
          ],
          "name": "default",
          "active": true
        },
        {
          "items": null,
          "name": "loggedIn",
          "active": false
        }
      ]
    },
    "regexInternalExternal": {
      "rules": [
        {
          "regex": "^(?!https?://).*",
          "internal": false
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/login.php.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/dialog.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/v1\\.0/.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/oauth.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/v2\\.0/.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com/checkpoint.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*facebook\\.com.*",
          "internal": true
        },
        {
          "regex": "https?://([-\\w]+\\.)*plus\\.google\\.com/share.*",
          "internal": false
        },
        {
          "regex": "https?://([-\\w]+\\.)*twitter\\.com/.*",
          "internal": false
        },
        {
          "regex": "https?://([-\\w]+\\.)*instagram\\.com/.*",
          "internal": false
        },
        {
          "regex": "https?://maps\\.google\\.com.*",
          "internal": false
        },
        {
          "regex": "https?://([-\\w]+\\.)*linkedin\\.com/.*",
          "internal": false
        },
        {
          "regex": "https?://([-\\w]+\\.)*google\\.com/maps/search/.*",
          "internal": false
        },
        {
          "regex": ".*",
          "internal": true
        }
      ],
      "active": false
    },
    "androidPullToRefresh": false,
    "iosPullToRefresh": true,
    "navigationTitles": {
      "titles": [
        {}
      ],
      "active": false
    },
    "toolbarNavigation": {
      "items": [
        {
          "system": "back",
          "title": "Back"
        },
        {
          "system": "forward",
          "title": "Forward"
        },
        {
          "system": "refresh"
        }
      ]
    },
    "androidShowRefreshButton": false,
    "deepLinkDomains": {
      "domains": [],
      "enableAndroidApplinks": false
    },
    "navigationLevels": {
      "levels": [
        {}
      ]
    }
  },
  "styling": {
    "transitionInteractiveDelayMax": 0.2,
    "menuAnimationDuration": 0.15,
    "androidShowSplash": true,
    "disableAnimations": false,
    "hideWebviewAlpha": 0.5,
    "showActionBar": true,
    "showNavigationBar": true,
    "iosSidebarFont": "Default",
    "androidHideTitleInActionBar": true,
    "navigationTitleImage": true,
    "iosTheme": "default",
    "androidTheme": "auto",
    "androidSidebarBackgroundColor": "#FFFFFF",
    "androidSidebarForegroundColor": "#1E496E",
    "androidActionBarBackgroundColor": "#FFFFFF",
    "androidActionBarForegroundColor": "#1E496E",
    "androidAccentColor": "#1E496E",
    "androidSidebarSeparatorColor": "#CCCCCC",
    "androidSidebarHighlightColor": "#1E496E",
    "androidShowLogoInSideBar": true,
    "androidShowAppNameInSideBar": true,
    "androidPullToRefreshColor": "#1E496E",
    "androidTabBarBackgroundColor": "#FFFFFF",
    "androidTabBarTextColor": "#949494",
    "androidTabBarIndicatorColor": "#1E496E",
    "androidStatusBarBackgroundColor": "#5C5C5C",
    "iosTintColor": "#1E496E",
    "iosTitleColor": "#1E496E",
    "iosSidebarTextColor": "#1E496E",
    "androidBackgroundColor": "#FFFFFF",
    "androidSwipeNavigationBackgroundColor": "#FFFFFF",
    "androidSwipeNavigationActiveColor": "#000000",
    "androidSwipeNavigationInactiveColor": "#666666",
    "androidActionBarBackgroundColorDark": "#333333",
    "androidStatusBarBackgroundColorDark": "#333333",
    "androidActionBarForegroundColorDark": "#FFFFFF",
    "androidAccentColorDark": "#666666",
    "androidBackgroundColorDark": "#333333",
    "androidSidebarForegroundColorDark": "#FFFFFF",
    "androidSidebarBackgroundColorDark": "#333333",
    "androidSidebarSeparatorColorDark": "#666666",
    "androidSidebarHighlightColorDark": "#FFFFFF",
    "androidPullToRefreshColorDark": "#FFFFFF",
    "androidTabBarTextColorDark": "#FFFFFF",
    "androidTabBarBackgroundColorDark": "#333333",
    "androidTabBarIndicatorColorDark": "#666666",
    "androidSwipeNavigationBackgroundColorDark": "#333333",
    "androidSwipeNavigationActiveColorDark": "#FFFFFF",
    "androidSwipeNavigationInactiveColorDark": "#666666"
  },
  "permissions": {
    "usesGeolocation": false,
    "androidDownloadToPublicStorage": true,
    "enableWebRTC": false
  },
  "performance": {
    "webviewPools": [
      {
        "urls": [
          {
            "disown": "reload"
          }
        ]
      }
    ]
  },
  "services": {
    "facebook": {
      "pluginName": ""
    }
  }
}

================================================
FILE: app/src/main/assets/custom-icons.json
================================================
{
  "gonative-icon": 59392
}


================================================
FILE: app/src/main/assets/offline.html
================================================
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
    <title>Device Offline</title>
    <style type="text/css">
      :root { --background-color: white; --primary-color: #1C1C1E; }
      [data-theme="dark"] { --background-color: #121212; --primary-color: #eee; }
      [data-theme="light"] { --background-color: white; --primary-color: #1C1C1E; }
      html, body{ background-color: var(--background-color) !important; }
      html, body, button { font-family: Arial, Helvetica, sans-serif; font-size: 18px; }
      div.container { color: var(--primary-color); position: relative; top: 100px; text-align: center; }
      #logo>svg>g>path{ fill: var(--primary-color); }
      button { padding: 10px 30px; margin: 4px 2px; }
    </style>
</head>
<body>
<div class="container">
    <div id="logo">
        <svg width="120" height="96" viewBox="0 0 120 96" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0_5510_497)"><path d="M118.275 87.9562L7.27647 0.958312C6.45147 0.313687 5.47272 0 4.50522 0C3.17022 0 1.84459 0.592125 0.959779 1.72294C-0.575284 3.68062 -0.234596 6.51 1.72253 8.04187L112.554 94.8731C114.523 96.4112 117.348 96.0596 118.871 94.1085C120.581 92.325 120.225 89.4937 118.275 87.9562Z" fill="black"/><path opacity="0.4" d="M36 53.8313C32.6869 53.8313 30 56.5163 30 59.6625V89.4937C30 92.8069 32.6869 95.4937 36 95.4937C39.3131 95.4937 42 92.8069 42 89.4937V59.6625C42 56.6813 39.3187 53.8313 36 53.8313ZM54 90C54 93.3131 56.6869 96 60 96C63.3131 96 66 93.3131 66 90V69.8625L54 60.4575V90ZM12 71.8313C8.68687 71.8313 6 74.5163 6 77.6625V89.4937C6 92.8069 8.68687 95.4937 12 95.4937C15.3131 95.4937 18 92.8069 18 89.4937V77.6625C18 74.6813 15.3131 71.8313 12 71.8313ZM60 36C57.9937 36 56.325 37.0313 55.2375 38.55L66 46.9875V42C66 38.6812 63.3187 36 60 36ZM107.831 0C104.518 0 101.831 2.68687 101.831 6V75.2063L113.831 84.6113V6C113.831 2.68687 111.319 0 107.831 0ZM78 90C78 93.3131 80.6869 96 84 96C87.3131 96 90 93.3131 90 90V88.6684L78 79.2634V90ZM84 18C80.6869 18 78 20.6869 78 24V56.3813L90 65.7862V24C90 20.6812 87.3187 18 84 18Z" fill="black"/></g><defs><clipPath id="clip0_5510_497"><rect width="120" height="96" fill="white"/></clipPath></defs></svg>
    </div>
    <span id="message">
        <p>No internet connection<br/>Check your connection and try again</p>
      </span>
    <button id="retryButton" type="button" onclick="gonative.webview.reload()">Retry</button>
</div>
<script>
      var message = document.getElementById('message');
      var retryButton = document.getElementById('retryButton')
      if (['ko', 'ko-kr', 'ko-kp'].indexOf(navigator.language.toLowerCase()) > -1) {
        // Korean
        message.innerHTML = '<p>인터넷 연결이 끊겼습니다.<br/>연결하고 다시 시도하시길 바랍니다.</p>';
        retryButton.innerText = '괜찮아';
      }
      function updateDarkMode(){
        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
          document.documentElement.setAttribute("data-theme", "dark");
        }
        else {
          document.documentElement.setAttribute("data-theme", "light");
        }
      }
      updateDarkMode();
      if(window.matchMedia){
        window.matchMedia('(prefers-color-scheme: dark)')
          .addEventListener('change', updateDarkMode);
      }
    </script>
</body>
</html>

================================================
FILE: app/src/main/java/io/gonative/android/ActionManager.java
================================================
package io.gonative.android;

import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.widget.LinearLayoutCompat;
import androidx.appcompat.widget.SearchView;
import androidx.core.content.ContextCompat;
import androidx.drawerlayout.widget.DrawerLayout;

import com.google.android.material.appbar.MaterialToolbar;

import io.gonative.android.icons.Icon;

import org.json.JSONArray;
import org.json.JSONObject;

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.regex.Pattern;

import io.gonative.gonative_core.AppConfig;

/**
 * Created by weiyin on 11/25/14.
 * Copyright 2014 GoNative.io LLC
 */
public class ActionManager {
    private static final String TAG = ActionManager.class.getName();
    private static final String ACTION_SHARE = "share";
    private static final String ACTION_REFRESH = "refresh";
    private static final String ACTION_SEARCH = "search";
    private static final int ACTIONBAR_ITEM_MARGIN = 132;

    private final MainActivity activity;
    private final HashMap<MenuItem, String>itemToUrl;
    private final int action_button_size;
    private final ActionBar actionBar;
    private final ImageView actionBarImageTitle;
    private final int colorForeground;
    private final int colorBackground;
    private boolean isRoot;
    private String currentMenuID;
    private LinearLayout header;
    private LinearLayoutCompat menuContainer;
    private RelativeLayout titleContainer;
    private boolean isOnSearchMode = false;
    private SearchView searchView;

    private int leftItemsCount = 0;
    private int rightItemsCount = 0;

    private String currentUrl;

    ActionManager(MainActivity activity) {
        this.activity = activity;
        this.itemToUrl = new HashMap<>();
        this.action_button_size = this.activity.getResources().getInteger(R.integer.action_button_size);
        this.actionBar = activity.getSupportActionBar();

        this.actionBarImageTitle = new ImageView(activity);
        this.actionBarImageTitle.setImageResource(R.drawable.ic_actionbar);

        this.colorForeground = activity.getResources().getColor(R.color.titleTextColor);
        this.colorBackground = activity.getResources().getColor(R.color.colorPrimary);
    }

    public void setupActionBar(boolean isRoot) {
        if (actionBar == null) return;

        this.isRoot = isRoot;
        AppConfig appConfig = AppConfig.getInstance(activity);

        // Change hamburger button to back arrow if window is not root
        if (!isRoot) {
            actionBar.setDisplayHomeAsUpEnabled(true);
            Drawable backArrow = ContextCompat.getDrawable(activity, R.drawable.abc_ic_ab_back_material);
            backArrow.setColorFilter(colorForeground, PorterDuff.Mode.SRC_ATOP);
            actionBar.setHomeAsUpIndicator(backArrow);
        }

        header = (LinearLayout) activity.getLayoutInflater().inflate(R.layout.actionbar_title, null);
        // why use a custom view and not setDisplayUseLogoEnabled and setLogo?
        // Because logo doesn't work!
        actionBar.setDisplayShowCustomEnabled(true);
        actionBar.setDisplayShowTitleEnabled(false);
        actionBar.setCustomView(header);
        ActionBar.LayoutParams params = (ActionBar.LayoutParams) header.getLayoutParams();
        params.width = ActionBar.LayoutParams.MATCH_PARENT;
        titleContainer = header.findViewById(R.id.title_container);

        menuContainer = header.findViewById(R.id.left_menu_container);

        ViewGroup.MarginLayoutParams titleContainerParams = (ViewGroup.MarginLayoutParams) titleContainer.getLayoutParams();
        titleContainerParams.rightMargin = ACTIONBAR_ITEM_MARGIN + 8;

        MaterialToolbar toolbar = activity.findViewById(R.id.toolbar);
        toolbar.setBackgroundColor(colorBackground);

        // fix title offset when side menu hamburger icon is not yet available
        if (appConfig.showNavigationMenu && isRoot) {
            menuContainer.getLayoutParams().width = 140;
        }
    }

    public void setupTitleDisplayForUrl(String url) {
        if (actionBar == null || url == null) return;
        AppConfig appConfig = AppConfig.getInstance(activity);
        this.currentUrl = url;

        boolean urlHasNavTitle = false;
        boolean urlHasActionMenu = false;

        // Check for Nav title
        HashMap<String, Object> urlNavTitle = appConfig.getNavigationTitleForUrl(url);
        if (urlNavTitle != null) {
            urlHasNavTitle = true;
        }

        // Check for Action Menus
        ArrayList<Pattern> regexes = appConfig.actionRegexes;
        ArrayList<String> ids = appConfig.actionIDs;
        if (regexes != null && ids != null) {
            for (int i = 0; i < regexes.size(); i++) {
                Pattern regex = regexes.get(i);
                if (regex.matcher(url).matches()) {
                    urlHasActionMenu = true;
                    break;
                }
            }
        }

        if (!appConfig.showActionBar && !appConfig.showNavigationMenu && !urlHasNavTitle && !urlHasActionMenu) {
            actionBar.hide();
        } else {
            if (urlHasNavTitle) {
                boolean showImage = true;
                if (urlNavTitle.containsKey("showImage")) showImage = (boolean) urlNavTitle.get("showImage");

                if (showImage) {
                    // Show image title
                    actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);
                    showTitleView(actionBarImageTitle);
                } else {
                    // Show text title
                    String title = activity.getTitle().toString();;
                    if (urlNavTitle.containsKey("title")) title = (String) urlNavTitle.get("title");
                    showTextActionBarTitle(title);
                }
            } else {
                showLogoInActionBar(appConfig.shouldShowNavigationTitleImageForUrl(currentUrl));
            }
            setupActionBarDisplay();
            actionBar.show();
        }
    }

    private void showLogoInActionBar(boolean show) {
        if (actionBar == null) return;
        if (show) {
            showTitleView(actionBarImageTitle);
        } else {
            // Show Text
            showTextActionBarTitle(activity.getTitle());
        }
    }

    public void showTextActionBarTitle(CharSequence title) {
        TextView textView = new TextView(activity);
        textView.setText(TextUtils.isEmpty(title) ? activity.getTitle() : title);
        textView.setTextSize(18);
        textView.setTypeface(null, Typeface.BOLD);
        textView.setMaxLines(1);
        textView.setEllipsize(TextUtils.TruncateAt.END);
        textView.setTextColor(colorForeground);
        showTitleView(textView);
    }

    public void showTitleView(View titleView) {
        if (actionBar == null) return;
        if (titleView == null) return;

        LinearLayout header = (LinearLayout) actionBar.getCustomView();

        if (header == null) return;

        // Remove Title Container child views if there is any
        titleContainer.removeAllViews();

        // Remove Title View parent if there is any
        if (titleView.getParent() != null) {
            ((ViewGroup) titleView.getParent()).removeView(titleView);
        }

        titleContainer.addView(titleView);
    }

    // Remove title offset once sidebar hamburger menu setup is complete
    public void cleanSidebarMenuTitleOffset() {
        if (menuContainer == null) return;
        menuContainer.getLayoutParams().width = 0;
    }

    public void checkActions(String url) {
        if (this.activity == null || url == null) return;

        AppConfig appConfig = AppConfig.getInstance(this.activity);

        ArrayList<Pattern> regexes = appConfig.actionRegexes;
        ArrayList<String> ids = appConfig.actionIDs;
        if (regexes == null || ids == null) {
            setMenuID(null);
            return;
        }

        for (int i = 0; i < regexes.size(); i++) {
            Pattern regex = regexes.get(i);
            if (regex.matcher(url).matches()) {
                setMenuID(ids.get(i));
                return;
            }
        }

        setMenuID(null);
    }

    private void setMenuID(String menuID) {
        boolean changed;

        if (this.currentMenuID == null) {
            changed = menuID != null;
        } else {
            changed = menuID == null || !this.currentMenuID.equals(menuID);
        }

        if (changed) {
            this.currentMenuID = menuID;
            this.activity.invalidateOptionsMenu();
        }
    }

    public void addActions(Menu menu) {
        this.itemToUrl.clear();
        this.rightItemsCount = 0;
        this.leftItemsCount = 0;

        AppConfig appConfig = AppConfig.getInstance(this.activity);
        if (appConfig.actions == null) return;
        menuContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;

        JSONArray actions = appConfig.actions.get(currentMenuID);
        if (actions == null || actions.length() == 0) {
            replaceLeftIcon(null);
        } else {
            if (actions.length() <= 2) {
                for (int itemID = 0; itemID < actions.length(); itemID++) {
                    JSONObject entry = actions.optJSONObject(itemID);
                    addRightButton(appConfig, menu, itemID, entry);
                }
            } else {
                for (int itemID = 0; itemID < actions.length(); itemID++) {
                    JSONObject entry = actions.optJSONObject(itemID);
                    if (itemID == 0) {
                        addLeftButton(appConfig, entry);
                    } else {
                        addRightButton(appConfig, menu, itemID, entry);
                    }
                }
            }
        }
        setupActionBarDisplay();
    }

    private void addLeftButton(AppConfig appConfig, JSONObject entry) {
        if (entry == null) return;

        String system = AppConfig.optString(entry, "system");
        String icon = AppConfig.optString(entry, "icon");
        String url = AppConfig.optString(entry, "url");

        if (!TextUtils.isEmpty(system)) {
            if (system.equalsIgnoreCase("refresh")) {
                if (TextUtils.isEmpty(icon)) {
                    icon = "fa-rotate-right";
                }
                Button refresh = createButtonMenu(icon);
                refresh.setOnClickListener(v -> this.activity.onRefresh());
                replaceLeftIcon(refresh);
            } else if (system.equalsIgnoreCase("share")) {
                if (TextUtils.isEmpty(icon)) {
                    icon = "fa-share";
                }
                Button share = createButtonMenu(icon);
                share.setOnClickListener(v -> this.activity.sharePage(null, null));
                replaceLeftIcon(share);
            } else if (system.equalsIgnoreCase("search")) {
                if (TextUtils.isEmpty(icon)) {
                    icon = "fa fa-search";
                }
                this.searchView = createSearchView(appConfig, icon, url, null, true);
                replaceLeftIcon(this.searchView);
            } else {
                addLeftCustomButton(icon, url);
            }
        } else {
            addLeftCustomButton(icon, url);
        }

        if (!appConfig.showNavigationMenu) {
            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) menuContainer.getLayoutParams();
            params.leftMargin = 35;
        }

        leftItemsCount++;
    }

    private void addLeftCustomButton(String icon, String url) {
        Button userButton = createButtonMenu(icon);
        userButton.setOnClickListener(v -> this.activity.loadUrl(url));
        replaceLeftIcon(userButton);
    }

    private void addRightButton(AppConfig appConfig, Menu menu, int itemID, JSONObject entry) {
        if (entry == null) return;

        String system = AppConfig.optString(entry, "system");
        String label = AppConfig.optString(entry, "label");
        String icon = AppConfig.optString(entry, "icon");
        String url = AppConfig.optString(entry, "url");

        if (!TextUtils.isEmpty(system)) {
            if (system.equalsIgnoreCase("refresh")) {

                if (TextUtils.isEmpty(icon)) {
                    icon = "fa-rotate-right";
                }

                Drawable refreshIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();

                String menuLabel = !TextUtils.isEmpty(label) ? label : "Refresh";

                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)
                        .setIcon(refreshIcon)
                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);

                itemToUrl.put(menuItem, ACTION_REFRESH);
            } else if (system.equalsIgnoreCase("share")) {

                if (TextUtils.isEmpty(icon)) {
                    icon = "fa-share";
                }

                Drawable refreshIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();;

                String menuLabel = !TextUtils.isEmpty(label) ? label : "Share";

                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)
                        .setIcon(refreshIcon)
                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);

                itemToUrl.put(menuItem, ACTION_SHARE);

            } else if (system.equalsIgnoreCase("search")) {

                if (TextUtils.isEmpty(icon)) {
                    icon = "fa fa-search";
                }

                String menuLabel = !TextUtils.isEmpty(label) ? label : "Search";

                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)
                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);

                this.searchView = createSearchView(appConfig, icon, url, menuItem, false);
                menuItem.setActionView(searchView);

                itemToUrl.put(menuItem, ACTION_SEARCH);
            } else {
                addRightCustomButton(menu, itemID, label, icon, url);
            }
        } else {
            addRightCustomButton(menu, itemID, label, icon, url);
        }

        rightItemsCount++;
    }

    private void addRightCustomButton(Menu menu, int itemID, String label, String icon, String url) {
        Drawable iconDrawable = null;
        if (icon != null) {
            iconDrawable = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();
        }
        MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, label)
                .setIcon(iconDrawable)
                .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);

        if (url != null) {
            this.itemToUrl.put(menuItem, url);
        }
    }

    private void replaceLeftIcon(View view) {
        if (menuContainer == null) return;
        menuContainer.removeAllViews();
        if (view != null) {
            menuContainer.addView(view);
            menuContainer.setVisibility(View.VISIBLE);
        } else {
            menuContainer.setVisibility(View.GONE);
        }
    }

    private Button createButtonMenu(String iconString) {
        Drawable icon = new Icon(activity, iconString, action_button_size, colorForeground).getDrawable();
        icon.setBounds(0, 0, 50, 50);
        LinearLayout tempView = (LinearLayout) LayoutInflater.from(activity).inflate(R.layout.button_menu, null);
        Button button = tempView.findViewById(R.id.menu_button);
        tempView.removeView(button);
        button.setCompoundDrawables(icon, null, null, null);
        return button;
    }

    private SearchView createSearchView(AppConfig appConfig, String icon, String url, MenuItem menuItem, boolean forLeftSide) {
        SearchView searchView = new SearchView(activity);

        // Set layout Params to WRAP_CONTENT
        ViewGroup.LayoutParams searchViewParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        searchView.setLayoutParams(searchViewParams);

        // Left Drawer Instance
        DrawerLayout drawerLayout = activity.getDrawerLayout();
        ActionBarDrawerToggle drawerToggle = activity.getDrawerToggle();

        // search item in action bar
        SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(androidx.appcompat.R.id.search_src_text);
        if (searchAutoComplete != null) {
            searchAutoComplete.setTextColor(colorForeground);
            int hintColor = colorForeground;
            hintColor = Color.argb(192, Color.red(hintColor), Color.green(hintColor),
                    Color.blue(hintColor));
            searchAutoComplete.setHintTextColor(hintColor);
        }

        searchView.setOnSearchClickListener(view -> {
            searchViewParams.width = ActionBar.LayoutParams.MATCH_PARENT;

            // Need to check this otherwise the app will crash
            if (!activity.isNotRoot() && appConfig.showNavigationMenu) {
                drawerToggle.setDrawerIndicatorEnabled(false);
                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);

                drawerToggle.setDrawerIndicatorEnabled(false);
                actionBar.setDisplayShowHomeEnabled(true);
            } else if (!activity.isNotRoot()) {
                actionBar.setDisplayHomeAsUpEnabled(true);
            }
            isOnSearchMode = true;
        });

        searchView.setOnCloseListener(() -> {
            if (forLeftSide) {
                titleContainer.setVisibility(View.VISIBLE);
            } else {
                header.setVisibility(View.VISIBLE);
                activity.invalidateOptionsMenu();
            }
            searchViewParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;

            activity.setMenuItemsVisible(true);
            if (!activity.isNotRoot() && appConfig.showNavigationMenu) {
                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
                actionBar.setDisplayShowHomeEnabled(false);
                drawerToggle.setDrawerIndicatorEnabled(true);
            } else if (!activity.isNotRoot()) {
                actionBar.setDisplayHomeAsUpEnabled(false);
            }
            return false;
        });

        // listener to process query
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String query) {
                if (!searchView.isIconified()) {
                    searchView.setIconified(true);
                }
                try {
                    String q = URLEncoder.encode(query, "UTF-8");
                    activity.loadUrl(url + q);
                } catch (UnsupportedEncodingException e) {
                    return true;
                }

                return true;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                // do nothing
                return true;
            }
        });

        // listener to collapse action view when soft keyboard is closed
        searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {
            @Override
            public void onFocusChange(View v, boolean hasFocus) {
                if (!hasFocus) {
                    if (!searchView.isIconified()) {
                        searchView.setIconified(true);
                    }
                }
            }
        });

        // Search view button icon and color
        ImageView searchIcon = searchView.findViewById(androidx.appcompat.R.id.search_button);
        if (searchIcon != null) {
            icon = !TextUtils.isEmpty(icon) ? icon : "fa fa-search";
            Drawable searchButtonNewIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();
            searchIcon.setImageDrawable(searchButtonNewIcon);
            searchIcon.setColorFilter(colorForeground);

            // Handling a bug when SearchView is expanded and setting other menu
            // visibility to false, SearchView would trigger unnecessary onClose event
            // Solution is to hide other menus first before expanding
            searchIcon.setOnClickListener(v -> {
                if (forLeftSide) {
                    activity.setMenuItemsVisible(false);
                    titleContainer.setVisibility(View.GONE);
                } else {
                    header.setVisibility(View.GONE);
                    activity.setMenuItemsVisible(false, menuItem);
                }
                // Expand SearchView, simulates onSearchViewClicked event
                searchView.setIconified(false);
            });
        }

        //Search view close button foreground color
        ImageView closeButtonImage = searchView.findViewById(androidx.appcompat.R.id.search_close_btn);
        if (closeButtonImage != null) {
            closeButtonImage.setColorFilter(colorForeground);
        }

        return searchView;
    }

    // Count left and right actionbar buttons to calculate side margins
    public void setupActionBarDisplay() {

        if (actionBar == null) return;

        AppConfig appConfig = AppConfig.getInstance(activity);

        // Add to temporary fields so actual items count would not be affected
        int tempLeftItemsCount = leftItemsCount;

        // Limit right menu count to three for margin
        int tempRightItemsCount = Math.min(rightItemsCount, 3);

        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) titleContainer.getLayoutParams();

        // Reset the margins
        params.rightMargin = 0;
        params.leftMargin = 0;

        if (isRoot) {
            if (appConfig.showNavigationMenu) {
                tempLeftItemsCount++;
            }

            if (tempLeftItemsCount > tempRightItemsCount) {
                int margin = tempLeftItemsCount - tempRightItemsCount;
                params.rightMargin = ACTIONBAR_ITEM_MARGIN * margin;
            } else {
                int margin = tempRightItemsCount - tempLeftItemsCount;
                params.leftMargin = ACTIONBAR_ITEM_MARGIN * margin;
            }
        } else {
            tempLeftItemsCount++;

            if (tempLeftItemsCount > tempRightItemsCount) {
                int margin = tempLeftItemsCount - tempRightItemsCount;
                params.rightMargin = ACTIONBAR_ITEM_MARGIN * margin;
            } else {
                int margin = tempRightItemsCount - tempLeftItemsCount;
                params.leftMargin = ACTIONBAR_ITEM_MARGIN * margin;
            }
        }
    }

    public boolean isOnSearchMode() {
        return isOnSearchMode;
    }

    public void setOnSearchMode(boolean onSearchMode) {
        isOnSearchMode = onSearchMode;
    }

    public void closeSearchView() {
        if (searchView == null) return;

        if (!searchView.isIconified()) {
            searchView.setIconified(true);
        }
    }

    public boolean onOptionsItemSelected(MenuItem item) {
        if (activity.getCurrentFocus() instanceof SearchView.SearchAutoComplete) {
            activity.getCurrentFocus().clearFocus();
        }
        String url = this.itemToUrl.get(item);
        if (url != null) {
            switch (url) {
                case ACTION_SHARE:
                    this.activity.sharePage(null, null);
                    return true;
                case ACTION_REFRESH:
                    this.activity.onRefresh();
                    return true;
                case ACTION_SEARCH:
                    // Ignore
                    return true;
            }

            this.activity.loadUrl(url);
            return true;
        } else {
            return false;
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/AppLinksActivity.java
================================================
package io.gonative.android;

import android.content.Intent;
import android.os.Bundle;

import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;

public class AppLinksActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        launchApp();
    }

    private void launchApp() {
        Intent intent = new Intent(this, MainActivity.class);
        if (getIntent().getData() != null) {
            intent.setData(getIntent().getData());
            intent.setAction(Intent.ACTION_VIEW);
        }
        startActivity(intent);
        finish();
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/AudioUtils.java
================================================
package io.gonative.android;

import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.util.Log;

public class AudioUtils {
    private static final String TAG = AudioUtils.class.getName();
    private static AudioFocusRequest initialFocusRequest;
    private static AudioFocusRequest focusRequest;
    private static AudioManager.OnAudioFocusChangeListener initialAudioFocusChangeListener;
    private static AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
    
    /**
     * @param mode - Accepts int for speaker mode:
     *             0 - phone speaker (default)
     *             1 - headset / wired device
     *             2 - bluetooth
     */
    public static void setUpAudioDevice(MainActivity mainActivity, int mode) {
        AudioManager mAudioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);
        if (mode == 2) {
            // bluetooth device
            mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
            mAudioManager.startBluetoothSco();
            mAudioManager.setBluetoothScoOn(true);
        } else if (mode == 1) {
            // wired device
            mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
            mAudioManager.stopBluetoothSco();
            mAudioManager.setBluetoothScoOn(false);
            mAudioManager.setSpeakerphoneOn(false);
        } else {
            // phone speaker
            mAudioManager.setMode(AudioManager.MODE_NORMAL);
            mAudioManager.stopBluetoothSco();
            mAudioManager.setBluetoothScoOn(false);
            mAudioManager.setSpeakerphoneOn(true);
        }
    }
    
    public static void reconnectToBluetooth(MainActivity mainActivity, AudioManager audioManager) {
        if (audioManager.isBluetoothScoAvailableOffCall() && !audioManager.isBluetoothScoOn()) {
            Log.d(TAG, "Resetting audio to bluetooth device");
            setUpAudioDevice(mainActivity, 2);
        }
    }
    
    /**
     * Listen to the first AUDIOFOCUS_GAIN before taking the audio input/output priority through requestAudioFocus()
     *
     * @param mainActivity
     */
    public static void initAudioFocusListener(MainActivity mainActivity) {
        int result;
        final Object focusLock = new Object();
        
        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);
        if (audioManager == null) {
            Log.w(TAG, "AudioManager is null. Aborting initAudioFocusListener()");
        }
        
        initialAudioFocusChangeListener = focusChange -> {
            if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
                synchronized (focusLock) {
                    Log.d(TAG, "AudioFocusListener GAINED. Try to request audio focus");
                    requestAudioFocus(mainActivity);
                    abandonFocusRequest(mainActivity);
                }
            }
        };
        
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
            result = audioManager.requestAudioFocus(initialAudioFocusChangeListener,
                    AudioManager.STREAM_VOICE_CALL,
                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
        } else {
            AudioAttributes playbackAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                    .build();
            initialFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                    .setAudioAttributes(playbackAttributes)
                    .setAcceptsDelayedFocusGain(true)
                    .setOnAudioFocusChangeListener(initialAudioFocusChangeListener)
                    .build();
            result = audioManager.requestAudioFocus(initialFocusRequest);
        }
        
        synchronized (focusLock) {
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                Log.d(TAG, "AudioFocusListener REQUEST GRANTED");
            }
        }
    }
    
    /**
     * Prioritizes the bluetooth device if available.
     * Reconnects the bluetooth device when the audio focus is lost as a workaround for aborted connections
     * due to AudioRecord.AUDIO_INPUT_FLAG_FAST denial.
     *
     * @param mainActivity
     */
    public static void requestAudioFocus(MainActivity mainActivity) {
        int result;
        final Object focusLock = new Object();
        
        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);
        if (audioManager == null) {
            Log.w(TAG, "AudioManager is null. Aborting requestAudioFocus()");
        }
        audioFocusChangeListener = focusChange -> {
            switch (focusChange) {
                case AudioManager.AUDIOFOCUS_GAIN:
                    synchronized (focusLock) {
                        Log.d(TAG, "AudioFocus GAINED. Try to connect bluetooth device");
                        reconnectToBluetooth(mainActivity, audioManager);
                    }
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                case AudioManager.AUDIOFOCUS_LOSS:
                    synchronized (focusLock) {
                        Log.d(TAG, "AudioFocus LOST. Try to reconnect bluetooth device");
                        reconnectToBluetooth(mainActivity, audioManager);
                    }
                    break;
            }
        };
        
        abandonFocusRequest(mainActivity);
        
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
            result = audioManager.requestAudioFocus(audioFocusChangeListener,
                    AudioManager.STREAM_VOICE_CALL,
                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
        } else {
            AudioAttributes playbackAttributes = new AudioAttributes.Builder()
                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                    .build();
            focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                    .setAudioAttributes(playbackAttributes)
                    .setAcceptsDelayedFocusGain(true)
                    .setOnAudioFocusChangeListener(audioFocusChangeListener)
                    .build();
            result = audioManager.requestAudioFocus(focusRequest);
        }
        
        synchronized (focusLock) {
            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                Log.d(TAG, "AudioFocus REQUEST GRANTED");
                reconnectToBluetooth(mainActivity, audioManager);
            }
        }
    }
    
    public static void abandonFocusRequest(MainActivity mainActivity) {
        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);
        if (audioManager == null) {
            Log.w(TAG, "AudioManager is null. Aborting abandonFocusRequest()");
        }
        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {
            if (initialAudioFocusChangeListener != null) {
                audioManager.abandonAudioFocus(initialAudioFocusChangeListener);
                initialAudioFocusChangeListener = null;
            }
            if (audioFocusChangeListener != null) {
                audioManager.abandonAudioFocus(audioFocusChangeListener);
                audioFocusChangeListener = null;
            }
        } else {
            if (initialFocusRequest != null) {
                audioManager.abandonAudioFocusRequest(initialFocusRequest);
                initialFocusRequest = null;
            }
            if (focusRequest != null) {
                audioManager.abandonAudioFocusRequest(focusRequest);
                focusRequest = null;
            }
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/ConfigPreferences.java
================================================
package io.gonative.android;

import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.text.TextUtils;

public class ConfigPreferences {
    private static final String APP_THEME_KEY = "io.gonative.android.appTheme";

    private Context context;
    private SharedPreferences sharedPreferences;

    public ConfigPreferences(Context context) {
        this.context = context;
    }

    private SharedPreferences getSharedPreferences() {
        if (this.sharedPreferences == null) {
            this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context);
        }
        return this.sharedPreferences;
    }

    public String getAppTheme() {
        SharedPreferences preferences = getSharedPreferences();
        return preferences.getString(APP_THEME_KEY, null);
    }

    public void setAppTheme(String appTheme) {
        SharedPreferences preferences = getSharedPreferences();
        if (TextUtils.isEmpty(appTheme)) {
            preferences.edit().remove(APP_THEME_KEY).commit();
        } else {
            preferences.edit().putString(APP_THEME_KEY, appTheme).commit();
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/ConfigUpdater.java
================================================
package io.gonative.android;

import android.content.Context;
import android.os.AsyncTask;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.OutputStreamWriter;
import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;

/**
 * Created by weiyin on 8/8/14.
 */
public class ConfigUpdater {
    private static final String TAG = ConfigUpdater.class.getName();

    private Context context;

    ConfigUpdater(Context context) {
        this.context = context;
    }

    public void registerEvent() {
        if (AppConfig.getInstance(context).disableEventRecorder) return;

        new EventTask(context).execute();
    }

    private static class EventTask extends AsyncTask<Void, Void, Void> {
        WeakReference<Context> contextReference;

        EventTask(Context context) {
            this.contextReference = new WeakReference<>(context);
        }

        @Override
        protected Void doInBackground(Void... params) {
            Context context = contextReference.get();
            if (context == null) return null;

            JSONObject json = new JSONObject(Installation.getInfo(context));

            try {
                json.put("event", "launch");
            } catch (JSONException e) {
                GNLog.getInstance().logError(TAG, e.getMessage(), e);
                return null;
            }

            try {
                URL url = new URL("https://events.gonative.io/api/events/new");
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("POST");
                connection.setRequestProperty("Content-Type", "application/json");
                connection.setDoOutput(true);
                connection.setDoInput(false); // we do not care about response
                OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8");
                writer.write(json.toString());
                writer.close();
                connection.connect();
                connection.getResponseCode();
                connection.disconnect();
            } catch (Exception e) {
                GNLog.getInstance().logError(TAG, e.getMessage(), e);
            }

            return null;
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/CustomHeaders.java
================================================
package io.gonative.android;

import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.provider.Settings;
import android.util.Base64;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import io.gonative.gonative_core.AppConfig;

/**
 * Created by weiyin on 5/1/17.
 */

public class CustomHeaders {
    public static Map<String, String> getCustomHeaders(Context context) {
        AppConfig appConfig = AppConfig.getInstance(context);
        if (appConfig.customHeaders == null) return null;

        HashMap<String, String> result = new HashMap<>();
        for (Map.Entry<String, String> entry : appConfig.customHeaders.entrySet()) {
            String key = entry.getKey();
            String val;
            try {
                val = interpolateValues(context, entry.getValue());
            } catch (UnsupportedEncodingException e) {
                val = null;
            }

            if (key != null & val != null) {
                result.put(key, val);
            }
        }

        return result;
    }

    private static String interpolateValues(Context context, String value) throws UnsupportedEncodingException {
        if (value == null) return null;

        if (value.contains("%DEVICEID%")) {
            @SuppressLint("HardwareIds")
            String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
            if (androidId == null) androidId = "";
            value = value.replace("%DEVICEID%", androidId);
        }

        if (value.contains("%DEVICENAME64%")) {
            // base 64 encoded name
            String manufacturer = Build.MANUFACTURER;
            String model = Build.MODEL;
            String name;
            if (model.startsWith(manufacturer)) {
                name = model;
            } else {
                name = manufacturer + " " + model;
            }

            String name64 = Base64.encodeToString(name.getBytes("UTF-8"), Base64.NO_WRAP);
            value = value.replace("%DEVICENAME64%", name64);
        }

        return value;
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/DownloadService.java
================================================
package io.gonative.android;

import android.app.Service;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;
import io.gonative.gonative_core.LeanUtils;

public class DownloadService extends Service {

    private static final String TAG = "DownloadService";
    private static final String EXTRA_DOWNLOAD_ID = "download_id";
    private static final String ACTION_CANCEL_DOWNLOAD = "action_cancel_download";
    private static final int BUFFER_SIZE = 4096;
    private static final int timeout = 5; // in seconds
    private final Handler handler = new Handler(Looper.getMainLooper());

    private final Map<Integer, DownloadTask> downloadTasks = new HashMap<>();
    private int downloadId = 0;
    private String userAgent;

    @Override
    public void onCreate() {
        super.onCreate();
        AppConfig appConfig = AppConfig.getInstance(this);
        this.userAgent = appConfig.userAgent;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) {
            int id = intent.getIntExtra(EXTRA_DOWNLOAD_ID, 0);
            cancelDownload(id);
        }
        return START_NOT_STICKY;
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new DownloadBinder();
    }

    public class DownloadBinder extends Binder {
        public DownloadService getService() {
            return DownloadService.this;
        }
    }

    public void startDownload(String url, String filename, String mimetype, boolean shouldSaveToGallery, boolean open, FileDownloader.DownloadLocation location) {
        DownloadTask downloadTask = new DownloadTask(url, filename, mimetype, shouldSaveToGallery, open, location);
        downloadTasks.put(downloadTask.getId(), downloadTask);
        downloadTask.startDownload();
    }

    public void cancelDownload(int downloadId) {
        DownloadTask downloadTask = downloadTasks.get(downloadId);
        if (downloadTask != null && downloadTask.isDownloading()) {
            downloadTask.cancelDownload();
        }
    }

    public void handleDownloadUri(FileDownloader.DownloadLocation location, Uri uri, String mimeType, boolean shouldSaveToGallery, boolean open, String filename) {
        if (uri == null) return;
        if (location == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {

            if (shouldSaveToGallery) {
                addFileToGallery(uri);
            }

            if (open) {
                viewFile(uri, mimeType);
            } else {
                handler.post(() -> {
                    if (shouldSaveToGallery) {
                        Toast.makeText(this, R.string.file_download_finished_gallery, Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(this, String.format(this.getString(R.string.file_download_finished_with_name), filename), Toast.LENGTH_SHORT).show();
                    }
                });
            }
        } else {
            if (open) {
                viewFile(uri, mimeType);
            } else {
                handler.post(() -> Toast.makeText(this, String.format(this.getString(R.string.file_download_finished_with_name), filename), Toast.LENGTH_SHORT).show());
            }
        }
    }

    private void viewFile(Uri uri, String mimeType) {
        try {
            Intent intent = new Intent(Intent.ACTION_VIEW);
            intent.setDataAndType(uri, mimeType);
            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
            startActivity(intent);
        } catch (ActivityNotFoundException e) {
            String message = getResources().getString(R.string.file_handler_not_found);
            handler.post(() -> {
                Toast.makeText(this, message, Toast.LENGTH_LONG).show();
            });
        } catch (Exception ex) {
            GNLog.getInstance().logError(TAG, "viewFile: Exception:", ex);
        }
    }

    private void addFileToGallery(Uri uri) {
        Log.d(TAG, "addFileToGallery: Adding to Albums . . .");
        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        mediaScanIntent.setData(uri);
        sendBroadcast(mediaScanIntent);
    }

    private class DownloadTask {
        private final int id;
        private final String url;
        private boolean isDownloading;
        private HttpURLConnection connection;
        private InputStream inputStream;
        private FileOutputStream outputStream;
        private File outputFile = null;
        private Uri downloadUri;
        private String filename;
        private String extension;
        private String mimetype;
        private boolean saveToGallery;
        private boolean openOnFinish;
        private final FileDownloader.DownloadLocation location;

        public DownloadTask(String url, String filename, String mimetype, boolean saveToGallery, boolean open, FileDownloader.DownloadLocation location) {
            this.id = downloadId++;
            this.url = url;
            this.filename = filename;
            this.mimetype = mimetype;
            this.isDownloading = false;
            this.saveToGallery = saveToGallery;
            this.openOnFinish = open;
            this.location = location;
        }

        public int getId() {
            return id;
        }

        public boolean isDownloading() {
            return isDownloading;
        }

        public void startDownload() {
            Log.d(TAG, "startDownload: Starting download");
            isDownloading = true;
            new Thread(() -> {
                Log.d(TAG, "startDownload: Thread started");
                try {
                    URL downloadUrl = new URL(url);
                    connection = (HttpURLConnection) downloadUrl.openConnection();
                    connection.setInstanceFollowRedirects(true);
                    connection.setRequestProperty("User-Agent", userAgent);
                    connection.setConnectTimeout(timeout * 1000);
                    connection.connect();

                    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
                        GNLog.getInstance().logError(TAG, "Server returned HTTP " + connection.getResponseCode()
                                + " " + connection.getResponseMessage());
                        isDownloading = false;
                        return;
                    }

                    String contentDisposition = connection.getHeaderField("Content-Disposition");

                    double fileSizeInMB = connection.getContentLength() / 1048576.0;
                    Log.d(TAG, "startDownload: File size in MB: " + fileSizeInMB);

                    if (connection.getHeaderField("Content-Type") != null)
                        mimetype = connection.getHeaderField("Content-Type");

                    if (!TextUtils.isEmpty(filename)) {
                       extension = FileDownloader.getFilenameExtension(filename);
                       if (TextUtils.isEmpty(extension)) {
                           extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimetype);
                       } else if (Objects.equals(filename, extension)) {
                           filename = "download";
                       } else {
                           filename = filename.substring(0, filename.length() - (extension.length() + 1));
                           mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
                       }
                    } else {
                        // guess file name and extension
                        String guessedName = LeanUtils.guessFileName(url,
                                contentDisposition,
                                mimetype);
                        int pos = guessedName.lastIndexOf('.');

                        if (pos == -1) {
                            filename = guessedName;
                            extension = "";
                        } else if (pos == 0) {
                            filename = "download";
                            extension = guessedName.substring(1);
                        } else {
                            filename = guessedName.substring(0, pos);
                            extension = guessedName.substring(pos + 1);
                        }

                        if (!TextUtils.isEmpty(extension)) {
                            // Update mimetype based on final filename extension
                            mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
                        }
                    }

                    if (location == FileDownloader.DownloadLocation.PRIVATE_INTERNAL &&
                            !TextUtils.isEmpty(contentDisposition) &&
                            contentDisposition.startsWith("inline"))
                        this.openOnFinish = true;

                    if (location == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {
                        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                            ContentResolver contentResolver = getApplicationContext().getContentResolver();
                            if (saveToGallery && mimetype.contains("image")) {
                                downloadUri = FileDownloader.createExternalFileUri(contentResolver, filename, mimetype, Environment.DIRECTORY_PICTURES);
                            } else {
                                downloadUri = FileDownloader.createExternalFileUri(contentResolver, filename, mimetype, Environment.DIRECTORY_DOWNLOADS);
                                saveToGallery = false;
                            }
                            if (downloadUri != null) {
                                outputStream = (FileOutputStream) contentResolver.openOutputStream(downloadUri);
                            }
                        } else {
                            if (saveToGallery) {
                                outputFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), filename, extension);
                            } else {
                                outputFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename, extension);
                            }
                            outputStream = new FileOutputStream(outputFile);
                        }
                    } else {
                        this.openOnFinish = true;
                        outputFile = FileDownloader.createOutputFile(getFilesDir(), filename, extension);
                        outputStream = new FileOutputStream(outputFile);
                    }

                    int fileLength = connection.getContentLength();
                    inputStream = connection.getInputStream();

                    byte[] buffer = new byte[BUFFER_SIZE];
                    int bytesRead;
                    int bytesDownloaded = 0;

                    while ((bytesRead = inputStream.read(buffer)) != -1 && isDownloading) {
                        outputStream.write(buffer, 0, bytesRead);
                        bytesDownloaded += bytesRead;
                        int progress = (int) (bytesDownloaded * 100 / fileLength);
                        Log.d(TAG, "startDownload: Download progress: " + progress);
                    }
                    if (!isDownloading && outputFile != null) {
                        outputFile.delete();
                        outputFile = null;
                    }
                } catch (IOException e) {
                    GNLog.getInstance().logError(TAG, "startDownload: ", e);
                } finally {
                    try {
                        if (inputStream != null) inputStream.close();
                        if (outputStream != null) outputStream.close();
                        if (connection != null) connection.disconnect();
                    } catch (IOException e) {
                        GNLog.getInstance().logError(TAG, "startDownload: ", e);
                    }
                    isDownloading = false;
                    if (downloadUri == null && outputFile != null) {
                        downloadUri = FileProvider.getUriForFile(DownloadService.this, DownloadService.this.getApplicationContext().getPackageName() + ".fileprovider", outputFile);
                    }
                    handleDownloadUri(location, downloadUri, mimetype, saveToGallery, openOnFinish, filename + "." + extension);
                }
            }).start();
        }

        public void cancelDownload() {
            isDownloading = false;
            Toast.makeText(DownloadService.this, getString(R.string.download_canceled) + " " + filename, Toast.LENGTH_SHORT).show();
        }
    }
}

================================================
FILE: app/src/main/java/io/gonative/android/FileDownloader.java
================================================
package io.gonative.android;

import android.Manifest;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.os.IBinder;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.DownloadListener;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.core.content.ContextCompat;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.LeanUtils;

/**
 * Created by weiyin on 6/24/14.
 */
public class FileDownloader implements DownloadListener {
    public enum DownloadLocation {
        PUBLIC_DOWNLOADS, PRIVATE_INTERNAL
    }

    private static final String TAG = FileDownloader.class.getName();
    private final MainActivity context;
    private final DownloadLocation defaultDownloadLocation;
    private final ActivityResultLauncher<String[]> requestPermissionLauncher;
    private UrlNavigation urlNavigation;
    private String lastDownloadedUrl;
    private DownloadService downloadService;
    private boolean isBound = false;
    private PreDownloadInfo preDownloadInfo;

    private final ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            DownloadService.DownloadBinder binder = (DownloadService.DownloadBinder) iBinder;
            downloadService = binder.getService();
            isBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            downloadService = null;
            isBound = false;
        }
    };

    FileDownloader(MainActivity context) {
        this.context = context;

        AppConfig appConfig = AppConfig.getInstance(this.context);
        if (appConfig.downloadToPublicStorage) {
            this.defaultDownloadLocation = DownloadLocation.PUBLIC_DOWNLOADS;
        } else {
            this.defaultDownloadLocation = DownloadLocation.PRIVATE_INTERNAL;
        }

        Intent intent = new Intent(context, DownloadService.class);
        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

        // initialize request permission launcher
        requestPermissionLauncher = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {

            if (isGranted.containsKey(Manifest.permission.WRITE_EXTERNAL_STORAGE) && Boolean.FALSE.equals(isGranted.get(Manifest.permission.WRITE_EXTERNAL_STORAGE))) {
                Toast.makeText(context, "Unable to save download, storage permission denied", Toast.LENGTH_SHORT).show();
                return;
            }

            if (preDownloadInfo != null && isBound) {
                if (preDownloadInfo.isBlob) {
                    context.getFileWriterSharer().downloadBlobUrl(preDownloadInfo.url, preDownloadInfo.filename, preDownloadInfo.open);
                } else {
                    downloadService.startDownload(preDownloadInfo.url, preDownloadInfo.filename, preDownloadInfo.mimetype, preDownloadInfo.shouldSaveToGallery, preDownloadInfo.open, defaultDownloadLocation);
                }
                preDownloadInfo = null;
            }
        });
    }

    @Override
    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
        if (urlNavigation != null) {
            urlNavigation.onDownloadStart();
        }

        if (context != null) {
            context.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    context.showWebview();
                }
            });
        }

        // get filename from content disposition
        String guessFilename = null;
        if (!TextUtils.isEmpty(contentDisposition)) {
             guessFilename = LeanUtils.guessFileName(url, contentDisposition, mimetype);
        }

        if (url.startsWith("blob:") && context != null) {

            boolean openAfterDownload = defaultDownloadLocation == DownloadLocation.PRIVATE_INTERNAL;

            if (requestRequiredPermission(new PreDownloadInfo(url, guessFilename, true, openAfterDownload))) {
                return;
            }

            context.getFileWriterSharer().downloadBlobUrl(url, guessFilename, openAfterDownload);
            return;
        }

        lastDownloadedUrl = url;

        // try to guess mimetype
        if (mimetype == null || mimetype.equalsIgnoreCase("application/force-download") ||
                mimetype.equalsIgnoreCase("application/octet-stream")) {
            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
            String extension = MimeTypeMap.getFileExtensionFromUrl(url);
            if (extension != null && !extension.isEmpty()) {
                String guessedMimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
                if (guessedMimeType != null) {
                    mimetype = guessedMimeType;
                }
            }
        }

        startDownload(url, guessFilename,  mimetype, false, false);
    }

    public void downloadFile(String url, String filename, boolean shouldSaveToGallery, boolean open) {
        if (TextUtils.isEmpty(url)) {
            Log.d(TAG, "downloadFile: Url empty!");
            return;
        }

        if (url.startsWith("blob:") && context != null) {

            if (defaultDownloadLocation == DownloadLocation.PRIVATE_INTERNAL) {
                open = true;
            }

            if (requestRequiredPermission(new PreDownloadInfo(url, filename, true, open))) {
                return;
            }
            context.getFileWriterSharer().downloadBlobUrl(url, filename, open);
            return;
        }

        String mimetype = "*/*";
        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
        if (extension != null && !extension.isEmpty()) {
            String guessedMimeType = mimeTypeMap.getMimeTypeFromExtension(extension);
            if (guessedMimeType != null) {
                mimetype = guessedMimeType;
            }
        }

        startDownload(url, filename, mimetype, shouldSaveToGallery, open);
    }

    private void startDownload(String downloadUrl, String filename, String mimetype, boolean shouldSaveToGallery, boolean open) {
        if (isBound) {
            if (requestRequiredPermission(new PreDownloadInfo(downloadUrl, filename, mimetype, shouldSaveToGallery, open, false))) return;
            downloadService.startDownload(downloadUrl, filename, mimetype, shouldSaveToGallery, open, defaultDownloadLocation);
        }
    }

    // Requests required permission depending on device's SDK version
    private boolean requestRequiredPermission(PreDownloadInfo preDownloadInfo) {

        List<String> permissions = new ArrayList<>();

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P &&
                ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED &&
                defaultDownloadLocation == DownloadLocation.PUBLIC_DOWNLOADS) {
            permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
        }

        if (permissions.size() > 0) {
            this.preDownloadInfo = preDownloadInfo;
            requestPermissionLauncher.launch(permissions.toArray(new String[] {}));
            return true;
        }
        return false;
    }

    public String getLastDownloadedUrl() {
        return lastDownloadedUrl;
    }

    public void setUrlNavigation(UrlNavigation urlNavigation) {
        this.urlNavigation = urlNavigation;
    }

    public void unbindDownloadService() {
        if (isBound) {
            context.unbindService(serviceConnection);
            isBound = false;
        }
    }

    public static Uri createExternalFileUri(ContentResolver contentResolver, String filename, String mimetype, String environment) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);
        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimetype);

        if (Objects.equals(environment, Environment.DIRECTORY_PICTURES)) {
            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);
            return contentResolver.insert(MediaStore.Images.Media.getContentUri("external"), contentValues);
        } else if (Objects.equals(environment, Environment.DIRECTORY_DOWNLOADS)) {
            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);
            return contentResolver.insert(MediaStore.Files.getContentUri("external"), contentValues);
        }
        return null;
    }

    public static File createOutputFile(File dir, String filename, String extension) {
        return new File(dir, FileDownloader.getUniqueFileName(filename + "." + extension, dir));
    }

    public static String getUniqueFileName(String fileName, File dir) {
        File file = new File(dir, fileName);

        if (!file.exists()) {
            return fileName;
        }

        int count = 1;
        String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));
        String ext = fileName.substring(fileName.lastIndexOf('.'));
        String newFileName = nameWithoutExt + "_" + count + ext;
        file = new File(dir, newFileName);

        while (file.exists()) {
            count++;
            newFileName = nameWithoutExt + "_" + count + ext;
            file = new File(dir, newFileName);
        }

        return file.getName();
    }

    public static String getFilenameExtension(String name) {
        int pos = name.lastIndexOf('.');
        if (pos == -1) {
            return null;
        } else if (pos == 0) {
            return name;
        } else {
            return name.substring(pos + 1);
        }
    }

    private static class PreDownloadInfo {
        String url;
        String filename;
        String mimetype;
        boolean shouldSaveToGallery;
        boolean open;
        boolean isBlob;

        public PreDownloadInfo(String url, String filename, String mimetype, boolean shouldSaveToGallery, boolean open, boolean isBlob) {
            this.url = url;
            this.filename = filename;
            this.mimetype = mimetype;
            this.shouldSaveToGallery = shouldSaveToGallery;
            this.open = open;
            this.isBlob = isBlob;
        }

        public PreDownloadInfo(String url, String filename, boolean isBlob, boolean open) {
            this.url = url;
            this.filename = filename;
            this.isBlob = isBlob;
            this.open = open;
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/FileUploadIntentsCreator.kt
================================================
package io.gonative.android

import android.annotation.SuppressLint
import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.os.Parcelable
import android.provider.MediaStore
import android.webkit.MimeTypeMap
import androidx.core.content.FileProvider
import io.gonative.gonative_core.AppConfig
import io.gonative.gonative_core.Utils
import java.io.File
import java.text.SimpleDateFormat
import java.util.*


class FileUploadIntentsCreator(val context: Context, val mimeTypeSpecs: Array<String>, val multiple: Boolean) {
    private val mimeTypes = hashSetOf<String>()
    private val appConfig = AppConfig.getInstance(context)
    private var packageManger = context.packageManager

    var currentCaptureUri: Uri? = null

    init {
        extractMimeTypes()
    }

    private fun extractMimeTypes() {
        mimeTypeSpecs.forEach { spec ->
            val specParts = spec.split("[,;\\s]")
            specParts.forEach {
                if (it.startsWith(".")) {
                    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.substring(1))
                    mimeType?.let { it1 -> mimeTypes.add(it1) }
                } else if (it.contains("/")) {
                    mimeTypes.add(it)
                }
            }
        }

        if (mimeTypes.isEmpty()) {
            mimeTypes.add("*/*")
        }
    }

    fun imagesAllowed(): Boolean {
        if (!Utils.isPermissionGranted(context as Activity, android.Manifest.permission.CAMERA)) return false
        return mimeTypes.contains("*/*") || mimeTypes.any { it.contains("image/") }
    }

    fun videosAllowed(): Boolean {
        if (!Utils.isPermissionGranted(context as Activity, android.Manifest.permission.CAMERA)) return false
        return mimeTypes.contains("*/*") || mimeTypes.any { it.contains("video/") }
    }

    private fun photoCameraIntents(): ArrayList<Intent> {
        val intents = arrayListOf<Intent>()

        if (!appConfig.directCameraUploads) {
            return intents
        }

        val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
        val imageFileName = "IMG_$timeStamp.jpg"
        val storageDir = this.context.filesDir
        val captureFile = File(storageDir, imageFileName)

        currentCaptureUri = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".fileprovider", captureFile);

        val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)
        for (resolve in resolveList) {
            val packageName = resolve.activityInfo.packageName
            val intent = Intent(captureIntent)
            intent.component = ComponentName(resolve.activityInfo.packageName, resolve.activityInfo.name)
            intent.setPackage(packageName)
            intent.putExtra(MediaStore.EXTRA_OUTPUT, currentCaptureUri)
            intents.add(intent)
        }

        return intents
    }

    private fun videoCameraIntents(): ArrayList<Intent> {
        val intents = arrayListOf<Intent>()

        if (!appConfig.directCameraUploads) {
            return intents
        }

        val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)
        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)
        for (resolve in resolveList) {
            val packageName = resolve.activityInfo.packageName
            val intent = Intent(captureIntent)
            intent.component = ComponentName(resolve.activityInfo.packageName, resolve.activityInfo.name)
            intent.setPackage(packageName)
            intents.add(intent)
        }

        return intents
    }

    private fun filePickerIntent(): Intent {
        var intent: Intent
        intent = Intent(Intent.ACTION_GET_CONTENT) // or ACTION_OPEN_DOCUMENT
        intent.type = mimeTypes.joinToString(", ")
        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
        intent.addCategory(Intent.CATEGORY_OPENABLE)

        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(intent)

        if (resolveList.isEmpty() && Build.MANUFACTURER.equals("samsung", ignoreCase = true)) {
            intent = Intent("com.sec.android.app.myfiles.PICK_DATA")
            intent.putExtra("CONTENT_TYPE", "*/*")
            intent.addCategory(Intent.CATEGORY_DEFAULT)
            return intent
        }

        return intent
    }

    fun cameraIntent(): Intent {
        val mediaIntents = if (imagesAllowed()) {
            photoCameraIntents()
        } else {
            videoCameraIntents()
        }
        return mediaIntents.first()
    }

    @SuppressLint("IntentReset")
    fun chooserIntent(): Intent {
        val directCaptureIntents = arrayListOf<Intent>()
        if (imagesAllowed()) {
            directCaptureIntents.addAll(photoCameraIntents())
        }
        if (videosAllowed()) {
            directCaptureIntents.addAll(videoCameraIntents())
        }

        val chooserIntent: Intent?
        val mediaIntent: Intent?

        if (imagesAllowed() xor videosAllowed()) {
            mediaIntent = getMediaInitialIntent()
            mediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
            chooserIntent = Intent.createChooser(mediaIntent, context.getString(R.string.choose_action))
        } else if (onlyImagesAndVideo() && !isGooglePhotosDefaultApp()) {
            mediaIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
            mediaIntent.type = "image/*, video/*"
            mediaIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*"))
            mediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)
            chooserIntent = Intent.createChooser(mediaIntent, context.getString(R.string.choose_action))
        } else {
            chooserIntent = Intent.createChooser(filePickerIntent(), context.getString(R.string.choose_action))
        }
        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, directCaptureIntents.toTypedArray<Parcelable>())

        return chooserIntent
    }

    private fun getMediaInitialIntent(): Intent {
        return if (imagesAllowed()) {
            Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        } else {
            Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
        }
    }

    private fun onlyImagesAndVideo(): Boolean {
        return mimeTypes.all { it.startsWith("image/") || it.startsWith("video/") }
    }

    private fun isGooglePhotosDefaultApp(): Boolean {
        val captureIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)

        return resolveList.size == 1 && resolveList.first().activityInfo.packageName == "com.google.android.apps.photos"
    }

    private fun listOfAvailableAppsForIntent(intent: Intent): List<ResolveInfo> {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            packageManger.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))
        } else {
            @Suppress("DEPRECATION")
            packageManger.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
        }
    }
}

================================================
FILE: app/src/main/java/io/gonative/android/FileWriterSharer.java
================================================
package io.gonative.android;

import android.Manifest;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Base64;
import android.util.Log;
import android.webkit.JavascriptInterface;
import android.webkit.MimeTypeMap;
import android.widget.Toast;

import androidx.core.content.FileProvider;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;
import io.gonative.gonative_core.LeanUtils;

public class FileWriterSharer {
    private static final String TAG = FileWriterSharer.class.getSimpleName();
    private static final long MAX_SIZE = 1024 * 1024 * 1024; // 1 gigabyte
    private static final String BASE64TAG = ";base64,";
    private final FileDownloader.DownloadLocation defaultDownloadLocation;
    private String downloadFilename;
    private boolean open = false;

    private static class FileInfo{
        public String id;
        public String name;
        public long size;
        public String mimetype;
        public String extension;
        public File savedFile;
        public Uri savedUri;
        public OutputStream fileOutputStream;
        public long bytesWritten;
    }

    private class JavascriptBridge {
        @JavascriptInterface
        public void postMessage(String jsonMessage) {
            Log.d(TAG, "got message " + jsonMessage);
            try {
                JSONObject json = new JSONObject(jsonMessage);
                String event = LeanUtils.optString(json, "event");
                if ("fileStart".equals(event)) {
                    onFileStart(json);
                } else if ("fileChunk".equals(event)) {
                    onFileChunk(json);
                } else if ("fileEnd".equals(event)) {
                    onFileEnd(json);
                } else if ("nextFileInfo".equals(event)) {
                    onNextFileInfo(json);
                } else {
                    GNLog.getInstance().logError(TAG, "Invalid event " + event);
                }
            } catch (JSONException e) {
                GNLog.getInstance().logError(TAG, "Error parsing message as json", e);
            } catch (IOException e) {
                GNLog.getInstance().logError(TAG, "IO Error", e);
            }
        }
    }

    private JavascriptBridge javascriptBridge;
    private MainActivity context;
    private Map<String, FileInfo> idToFileInfo;
    private String nextFileName;

    public FileWriterSharer(MainActivity context) {
        this.javascriptBridge = new JavascriptBridge();
        this.context = context;
        this.idToFileInfo = new HashMap<>();

        AppConfig appConfig = AppConfig.getInstance(this.context);
        if (appConfig.downloadToPublicStorage) {
            this.defaultDownloadLocation = FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS;
        } else {
            this.defaultDownloadLocation = FileDownloader.DownloadLocation.PRIVATE_INTERNAL;
        }
    }

    public JavascriptBridge getJavascriptBridge() {
        return javascriptBridge;
    }

    public void downloadBlobUrl(String url, String filename, boolean open) {
        if (url == null || !url.startsWith("blob:")) {
            return;
        }

        this.downloadFilename = filename;
        this.open = open;

        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            BufferedInputStream is = new BufferedInputStream(context.getAssets().open("BlobDownloader.js"));
            IOUtils.copy(is, baos);
            String js = baos.toString();
            context.runJavascript(js);
            js = "gonativeDownloadBlobUrl(" + LeanUtils.jsWrapString(url) + ")";
            context.runJavascript(js);
        } catch (IOException e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }
    }

    private void onFileStart(JSONObject message) throws IOException {
        String identifier = LeanUtils.optString(message, "id");
        if (identifier == null || identifier.isEmpty()) {
            GNLog.getInstance().logError(TAG, "Invalid file id");
            return;
        }

        String fileName;
        String extension = null;
        String type = null;

        if (!TextUtils.isEmpty(downloadFilename)) {
            extension = FileDownloader.getFilenameExtension(downloadFilename);
            if (!TextUtils.isEmpty(extension)) {
                if (Objects.equals(extension, downloadFilename)) {
                    fileName = "download";
                } else {
                    fileName = downloadFilename.substring(0, downloadFilename.length() - (extension.length() + 1));
                }
                type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
            } else {
                fileName = downloadFilename;
            }
        } else {
            fileName = LeanUtils.optString(message, "name");
            if (fileName == null || fileName.isEmpty()) {
                if (this.nextFileName != null) {
                    fileName = this.nextFileName;
                    this.nextFileName = null;
                } else {
                    fileName = "download";
                }
            }
        }

        long fileSize = message.optLong("size", -1);
        if (fileSize <= 0 || fileSize > MAX_SIZE) {
            GNLog.getInstance().logError(TAG, "Invalid file size");
            return;
        }

        if (TextUtils.isEmpty(type)) {
            type = LeanUtils.optString(message, "type");
            if (TextUtils.isEmpty(type)) {
                GNLog.getInstance().logError(TAG, "Invalid file type");
                return;
            }
        }

        if (TextUtils.isEmpty(extension)) {
            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
            extension = mimeTypeMap.getExtensionFromMimeType(type);
        }

        final FileInfo info = new FileInfo();
        info.id = identifier;
        info.name = fileName;
        info.size = fileSize;
        info.mimetype = type;
        info.extension = extension;

        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && defaultDownloadLocation == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {
            // request permissions
            context.getPermission(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, (permissions, grantResults) -> {
                try {
                    onFileStartAfterPermission(info, grantResults[0] == PackageManager.PERMISSION_GRANTED);
                    final String js = "gonativeGotStoragePermissions()";
                    context.runOnUiThread(() -> context.runJavascript(js));
                } catch (IOException e) {
                    GNLog.getInstance().logError(TAG, "IO Error", e);
                }
            });
        } else {
            onFileStartAfterPermission(info, true);
            final String js = "gonativeGotStoragePermissions()";
            context.runOnUiThread(() -> context.runJavascript(js));
        }
    }

    private void onFileStartAfterPermission(FileInfo info, boolean granted) throws IOException {
        if (granted && defaultDownloadLocation == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {
            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
                ContentResolver contentResolver = context.getApplicationContext().getContentResolver();
                Uri uri = FileDownloader.createExternalFileUri(contentResolver, info.name, info.mimetype, Environment.DIRECTORY_DOWNLOADS);
                if (uri != null) {
                    info.fileOutputStream = contentResolver.openOutputStream(uri);
                    info.savedUri = uri;
                }
            } else {
                info.savedFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), info.name, info.extension);
                info.fileOutputStream = new BufferedOutputStream(new FileOutputStream(info.savedFile));
            }
        } else {
            info.savedFile = FileDownloader.createOutputFile(context.getFilesDir(), info.name, info.extension);
            info.fileOutputStream = new BufferedOutputStream(new FileOutputStream(info.savedFile));
        }
        info.bytesWritten = 0;
        this.idToFileInfo.put(info.id, info);
    }

    private void onFileChunk(JSONObject message) throws IOException {
        String identifier = LeanUtils.optString(message, "id");
        if (identifier == null || identifier.isEmpty()) {
            return;
        }

        FileInfo fileInfo = this.idToFileInfo.get(identifier);
        if (fileInfo == null) {
            return;
        }

        String data = LeanUtils.optString(message, "data");
        if (data == null) {
            return;
        }

        int idx = data.indexOf(BASE64TAG);
        if (idx == -1) {
            return;
        }

        idx += BASE64TAG.length();
        byte[] chunk = Base64.decode(data.substring(idx), Base64.DEFAULT);

        if (fileInfo.bytesWritten + chunk.length > fileInfo.size) {
            GNLog.getInstance().logError(TAG, "Received too many bytes. Expected " + fileInfo.size);
            try {
                fileInfo.fileOutputStream.close();
                fileInfo.savedFile.delete();
                this.idToFileInfo.remove(identifier);
            } catch (Exception ignored) {

            }

            return;
        }

        fileInfo.fileOutputStream.write(chunk);
        fileInfo.bytesWritten += chunk.length;
    }

    private void onFileEnd(JSONObject message) throws IOException {
        String identifier = LeanUtils.optString(message, "id");
        if (identifier == null || identifier.isEmpty()) {
            GNLog.getInstance().logError(TAG, "Invalid identifier " + identifier + " for fileEnd");
            return;
        }

        final FileInfo fileInfo = this.idToFileInfo.get(identifier);
        if (fileInfo == null) {
            GNLog.getInstance().logError(TAG, "Invalid identifier " + identifier + " for fileEnd");
            return;
        }

        fileInfo.fileOutputStream.close();

        if (open) {
            context.runOnUiThread(() -> {
                if (fileInfo.savedUri == null && fileInfo.savedFile != null) {
                    fileInfo.savedUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + ".fileprovider", fileInfo.savedFile);
                }
                if (fileInfo.savedUri == null) return;

                Intent intent = getIntentToOpenFile(fileInfo.savedUri, fileInfo.mimetype);
                try {
                    context.startActivity(intent);
                } catch (ActivityNotFoundException e) {
                    String message1 = context.getResources().getString(R.string.file_handler_not_found);
                    Toast.makeText(context, message1, Toast.LENGTH_LONG).show();
                }
            });
        } else {
            String downloadCompleteMessage = fileInfo.name != null && !fileInfo.name.isEmpty()
                    ? String.format(context.getString(R.string.file_download_finished_with_name), fileInfo.name + '.' + fileInfo.extension)
                    : context.getString(R.string.file_download_finished);
            Toast.makeText(context, downloadCompleteMessage, Toast.LENGTH_SHORT).show();
        }
    }

    private Intent getIntentToOpenFile(Uri uri, String mimetype) {
        Intent intent = new Intent(Intent.ACTION_VIEW);
        intent.setDataAndType(uri, mimetype);
        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);
        return intent;
    }

    private void onNextFileInfo(JSONObject message) {
        String name = LeanUtils.optString(message, "name");
        if (name == null || name.isEmpty()) {
            GNLog.getInstance().logError(TAG, "Invalid name for nextFileInfo");
            return;
        }
        this.nextFileName = name;
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/GoNativeApplication.java
================================================
package io.gonative.android;

import android.os.Message;
import android.webkit.ValueCallback;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatDelegate;
import androidx.multidex.MultiDexApplication;

import java.util.List;
import java.util.Map;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.Bridge;
import io.gonative.gonative_core.BridgeModule;
import io.gonative.gonative_core.GNLog;

/**
 * Created by weiyin on 9/2/15.
 * Copyright 2014 GoNative.io LLC
 */
public class GoNativeApplication extends MultiDexApplication {

    private LoginManager loginManager;
    private RegistrationManager registrationManager;
    private WebViewPool webViewPool;
    private Message webviewMessage;
    private ValueCallback webviewValueCallback;
    private GoNativeWindowManager goNativeWindowManager;
    private List<BridgeModule> plugins;
    private final static String TAG = GoNativeApplication.class.getSimpleName();
    public final Bridge mBridge = new Bridge(this) {
        @Override
        protected List<BridgeModule> getPlugins() {
            if (GoNativeApplication.this.plugins == null) {
                GoNativeApplication.this.plugins = new PackageList(GoNativeApplication.this).getPackages();
            }

            return  GoNativeApplication.this.plugins;
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);

        mBridge.onApplicationCreate(this);

        AppConfig appConfig = AppConfig.getInstance(this);
        if (appConfig.configError != null) {
            Toast.makeText(this, "Invalid appConfig json", Toast.LENGTH_LONG).show();
            GNLog.getInstance().logError(TAG, "AppConfig error", appConfig.configError);
        }

        this.loginManager = new LoginManager(this);

        if (appConfig.registrationEndpoints != null) {
            this.registrationManager = new RegistrationManager(this);
            registrationManager.processConfig(appConfig.registrationEndpoints);
        }

        // some global webview setup
        WebViewSetup.setupWebviewGlobals(this);

        webViewPool = new WebViewPool();

        goNativeWindowManager = new GoNativeWindowManager();
    }

    public LoginManager getLoginManager() {
        return loginManager;
    }

    public RegistrationManager getRegistrationManager() {
        return registrationManager;
    }

    public WebViewPool getWebViewPool() {
        return webViewPool;
    }

    public Message getWebviewMessage() {
        return webviewMessage;
    }

    public void setWebviewMessage(Message webviewMessage) {
        this.webviewMessage = webviewMessage;
    }

    public Map<String, Object> getAnalyticsProviderInfo() {
        return mBridge.getAnalyticsProviderInfo();
    }

    // Needed for Crosswalk
    @SuppressWarnings("unused")
    public ValueCallback getWebviewValueCallback() {
        return webviewValueCallback;
    }

    public void setWebviewValueCallback(ValueCallback webviewValueCallback) {
        this.webviewValueCallback = webviewValueCallback;
    }

    public GoNativeWindowManager getWindowManager() {
        return goNativeWindowManager;
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/GoNativeWindowManager.java
================================================
package io.gonative.android;

import android.text.TextUtils;

import java.util.LinkedHashMap;
import java.util.Map;

public class GoNativeWindowManager {
    private final LinkedHashMap<String, ActivityWindow> windows;
    private ExcessWindowsClosedListener excessWindowsClosedListener;

    public GoNativeWindowManager() {
        windows = new LinkedHashMap<>();
    }

    public void addNewWindow(String activityId, boolean isRoot) {
        this.windows.put(activityId, new ActivityWindow(activityId, isRoot));
    }

    public void removeWindow(String activityId) {
        this.windows.remove(activityId);

        if (excessWindowsClosedListener != null && windows.size() <= 1) {
            excessWindowsClosedListener.onAllExcessWindowClosed();
        }
    }

    public void setOnExcessWindowClosedListener(ExcessWindowsClosedListener listener) {
        this.excessWindowsClosedListener = listener;
    }

    public ActivityWindow getActivityWindowInfo(String activityId) {
        return windows.get(activityId);
    }

    public void setUrlLevel(String activityId, int urlLevel) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            window.setUrlLevels(urlLevel, window.parentUrlLevel);
        }
    }

    public int getUrlLevel(String activityId) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            return window.urlLevel;
        }
        return -1;
    }

    public void setParentUrlLevel(String activityId, int parentLevel) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            window.setUrlLevels(window.urlLevel, parentLevel);
        }
    }

    public int getParentUrlLevel(String activityId) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            return window.parentUrlLevel;
        }
        return -1;
    }

    public void setUrlLevels(String activityId, int urlLevel, int parentLevel) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            window.setUrlLevels(urlLevel, parentLevel);
        }
    }

    public boolean isRoot(String activityId) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            return window.isRoot;
        }
        return false;
    }

    public void setAsNewRoot(String activityId) {
        for (Map.Entry<String, ActivityWindow> entry : windows.entrySet()) {
            ActivityWindow window = entry.getValue();
            if (TextUtils.equals(activityId, entry.getKey())) {
                window.isRoot = true;
            } else {
                window.isRoot = false;
            }
        }
    }

    public void setIgnoreInterceptMaxWindows(String activityId, boolean ignore) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            window.ignoreInterceptMaxWindows = ignore;
        }
    }

    public boolean isIgnoreInterceptMaxWindows(String activityId) {
        ActivityWindow window = windows.get(activityId);
        if (window != null) {
            return window.ignoreInterceptMaxWindows;
        }
        return false;
    }

    public int getWindowCount() {
        return windows.size();
    }

    // Returns ID of the next window after root as Excess window
    public String getExcessWindow() {
        for (Map.Entry<String, ActivityWindow> entry : windows.entrySet()) {
            ActivityWindow window = entry.getValue();
            if (window.isRoot) continue;
            return window.id;
        }
        return null;
    }

    public static class ActivityWindow {
        private final String id;
        private boolean isRoot;
        private int urlLevel;
        private int parentUrlLevel;
        private boolean ignoreInterceptMaxWindows;

        ActivityWindow(String id, boolean isRoot) {
            this.id = id;
            this.isRoot = isRoot;
            this.urlLevel = -1;
            this.parentUrlLevel = -1;
        }

        public void setUrlLevels(int urlLevel, int parentUrlLevel) {
            this.urlLevel = urlLevel;
            this.parentUrlLevel = parentUrlLevel;
        }

        @Override
        public String toString() {
            return "id=" + id + "\n" +
                    "isRoot=" + isRoot + "\n" +
                    "urlLevel=" + urlLevel + "\n" +
                    "parentUrlLevel=" + parentUrlLevel;
        }
    }


    interface ExcessWindowsClosedListener {
        void onAllExcessWindowClosed();
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/HtmlIntercept.java
================================================
package io.gonative.android;

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import java.util.Map;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;
import io.gonative.gonative_core.GoNativeWebviewInterface;

/**
 * Created by weiyin on 1/29/16.
 */

public class HtmlIntercept {
    private static final String TAG = HtmlIntercept.class.getName();

    private Context context;
    private String interceptUrl;
    private String JSBridgeScript;
    private String redirectedUrl;

    // track whether we have intercepted a page at all. We will always try to intercept the first time,
    // because interceptUrl may not have been set if restoring from a bundle.
    private boolean hasIntercepted = false;

    HtmlIntercept(Context context) {
        this.context = context;
    }

    public void setInterceptUrl(String interceptUrl) {
        this.interceptUrl = interceptUrl;
    }

    public WebResourceResponse interceptHtml(GoNativeWebviewInterface view, String url, String referer) {

        AppConfig appConfig = AppConfig.getInstance(context);
        if (!appConfig.interceptHtml && (appConfig.customHeaders == null || appConfig.customHeaders.isEmpty())) return null;

        if (!hasIntercepted) {
            interceptUrl = url;
            hasIntercepted = true;
        }
        if (!urlMatches(interceptUrl, url)) return null;

        InputStream is = null;
        ByteArrayOutputStream baos = null;

        try {
            URL parsedUrl = new URL(url);
            String protocol = parsedUrl.getProtocol();
            if (!protocol.equalsIgnoreCase("http") && !protocol.equalsIgnoreCase("https")) return null;

            HttpURLConnection connection = (HttpURLConnection)parsedUrl.openConnection();
            connection.setInstanceFollowRedirects(false);
            String customUserAgent = appConfig.userAgentForUrl(parsedUrl.toString());
            if (customUserAgent != null) {
                connection.setRequestProperty("User-Agent", customUserAgent);
            } else {
                connection.setRequestProperty("User-Agent", appConfig.userAgent);
            }
            connection.setRequestProperty("Cache-Control", "no-cache");

            if (referer != null) {
                connection.setRequestProperty("Referer", referer);
            }

            Map<String, String> customHeaders = CustomHeaders.getCustomHeaders(context);
            if (customHeaders != null) {
                for (Map.Entry<String, String> entry : customHeaders.entrySet()) {
                    connection.setRequestProperty(entry.getKey(), entry.getValue());
                }
            }

            connection.connect();
            int responseCode = connection.getResponseCode();

            if (responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
                    responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||
                    responseCode == HttpURLConnection.HTTP_SEE_OTHER ||
                    responseCode == 307) {
                // Get redirect URL to be loaded directly to webview, return blank resource which we cancel on UrlNavigation.onPageStart()
                // We cannot pass headers in webresourceresponse until Android API 21, and we cannot return null
                // or else the webview will handle the request entirely without intercept
                String location = connection.getHeaderField("Location");

                // validate location as URL
                try {
                    new URL(location);
                } catch (MalformedURLException ex) {
                    URL base = new URL(url);
                    location = new URL(base, location).toString();
                }

                if (!TextUtils.isEmpty(location)) {
                    this.redirectedUrl = url;
                    MainActivity mainActivity = (MainActivity) context;
                    WebView webView = (WebView) mainActivity.getWebView();
                    String finalLocation = location; // needed, for this should be effectively final
                    webView.post(() -> webView.loadUrl(finalLocation));
                }
                return new WebResourceResponse("text/html", "utf-8", new ByteArrayInputStream("".getBytes()));
            }

            String mimetype = connection.getContentType();
            if (mimetype == null) {
                try {
                    is = new BufferedInputStream(connection.getInputStream());
                } catch (IOException e) {
                    is = new BufferedInputStream(connection.getErrorStream());
                }
                mimetype = HttpURLConnection.guessContentTypeFromStream(is);
            }

            // if not html, then return null so that webview loads directly.
            if (mimetype == null || !mimetype.startsWith("text/html"))
                return null;

            // get and intercept the data
            String characterEncoding = getCharset(mimetype);
            if (characterEncoding == null) {
                characterEncoding = "UTF-8";
            } else if (characterEncoding.toLowerCase().equals("iso-8859-1")) {
                // windows-1252 is a superset of ios-8859-1 that supports the euro symbol €.
                // The html5 spec actually maps "iso-8859-1" to windows-1252 encoding
                characterEncoding = "windows-1252";
            }

            if (is == null) {
                try {
                    is = new BufferedInputStream(connection.getInputStream());
                } catch (IOException e) {
                    is = new BufferedInputStream(connection.getErrorStream());
                }
            }

            int initialLength = connection.getContentLength();
            if (initialLength < 0)
                initialLength = UrlNavigation.DEFAULT_HTML_SIZE;

            baos = new ByteArrayOutputStream(initialLength);
            IOUtils.copy(is, baos);
            String origString;
            try {
                origString = baos.toString(characterEncoding);
            } catch (UnsupportedEncodingException e){
                // Everything should support UTF-8
                origString = baos.toString("UTF-8");
            }

            // modify the string!
            String newString;
            int insertPoint = origString.indexOf("</head>");
            if (insertPoint >= 0) {
                StringBuilder builder = new StringBuilder(initialLength);
                builder.append(origString.substring(0, insertPoint));
                if (appConfig.customCSS != null || appConfig.androidCustomCSS != null) {
                    builder.append("<style>");
                    if(appConfig.customCSS != null)
                        builder.append(appConfig.customCSS).append(" ");
                    if(appConfig.androidCustomCSS != null)
                        builder.append(appConfig.androidCustomCSS);
                    builder.append("</style>");
                }

                if (appConfig.customJS != null || appConfig.androidCustomJS != null) {
                    builder.append("<script>");
                    if(appConfig.customJS != null)
                        builder.append(appConfig.customJS).append(" ");
                    if(appConfig.androidCustomJS != null)
                        builder.append(appConfig.androidCustomJS);
                    builder.append("</script>");
                }

                if (appConfig.stringViewport != null) {
                    builder.append("<meta name=\"viewport\" content=\"");
                    builder.append(TextUtils.htmlEncode(appConfig.stringViewport));
                    builder.append("\" />");
                }
                if (!Double.isNaN(appConfig.forceViewportWidth)) {
                    if (appConfig.zoomableForceViewport) {
                        builder.append(String.format(Locale.US, "<meta name=\"viewport\" content=\"width=%f,maximum-scale=1.0\" />",
                                appConfig.forceViewportWidth));
                    }
                    else {
                        // we want to use user-scalable=no, but android has a bug that sets scale to
                        // 1.0 if user-scalable=no. The workaround to is calculate the scale and set
                        // it for initial, minimum, and maximum.
                        // http://stackoverflow.com/questions/12723844/android-viewport-setting-user-scalable-no-breaks-width-zoom-level-of-viewpor
                        double webViewWidth = view.getWidth() / context.getResources().getDisplayMetrics().density;
                        double viewportWidth = appConfig.forceViewportWidth;
                        double scale = webViewWidth / viewportWidth;
                        builder.append(String.format(Locale.US, "<meta name=\"viewport\" content=\"width=%f,initial-scale=%f,minimum-scale=%f,maximum-scale=%f\" />",
                                viewportWidth, scale, scale, scale));
                    }
                }

                builder.append(origString.substring(insertPoint));
                newString = builder.toString();
            }
            else {
                Log.d(TAG, "could not find closing </head> tag");
                newString = origString;
            }

            return new WebResourceResponse("text/html", "UTF-8",
                    new ByteArrayInputStream(newString.getBytes("UTF-8")));
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.toString(), e);
            return null;
        } finally {
            IOUtils.close(is);
            IOUtils.close(baos);
        }
    }

    // Do these urls match, ignoring trailing slash in path
    private static boolean urlMatches(String url1, String url2) {
        if (url1 == null || url2 == null) return false;

        try {
            URL parsed1 = new URL(url1);
            URL parsed2 = new URL(url2);

            if (stringsNotEqual(parsed1.getProtocol(), parsed2.getProtocol())) return false;

            if (stringsNotEqual(parsed1.getAuthority(), parsed2.getAuthority())) return false;

            if (stringsNotEqual(parsed1.getQuery(), parsed2.getQuery())) return false;

            String path1 = parsed1.getPath();
            String path2 = parsed2.getPath();
            if (path1 == null) path1 = "";
            if (path2 == null) path2 = "";

            int lengthDiff = path2.length() - path2.length();
            if (lengthDiff > 1 || lengthDiff < -1) return false;
            if (lengthDiff == 0) return path1.equals(path2);
            if (lengthDiff == 1) {
                return path2.equals(path1 + "/");
            }

            // lengthDiff == -1
            return path1.equals(path2 + "/");
        } catch (MalformedURLException e) {
            return false;
        }
    }

    private static boolean stringsNotEqual(String s1, String s2) {
        return !(s1 == null ? s2 == null : s1.equals(s2));
    }

    private static String getCharset(String contentType) {
        if (contentType == null || contentType.isEmpty()) {
            return null;
        }

        String[] tokens = contentType.split("; *");
        for (String s : tokens) {
            if (s.startsWith("charset=")) {
                return s.substring("charset=".length());
            }
        }

        return null;
    }

    public String getRedirectedUrl() {
        return redirectedUrl;
    }

    public void setRedirectedUrl(String redirectUrl) {
        this.redirectedUrl = redirectUrl;
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/IOUtils.java
================================================
package io.gonative.android;


import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

import io.gonative.gonative_core.GNLog;

public class IOUtils {
    private static final String TAG = IOUtils.class.getName();

	public static void copy(InputStream in, OutputStream out) throws IOException{
		byte[] buf = new byte[1024];
	    int len;
	    while ((len = in.read(buf)) > 0) {
	        out.write(buf, 0, len);
	    }
	}
	
	public static void close(Closeable c) {
        if (c == null) return;
        try {
            c.close();
        } catch (IOException e){
            GNLog.getInstance().logError(TAG, e.toString(), e);
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/Installation.java
================================================
package io.gonative.android;

import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.util.Log;

import androidx.core.app.ActivityCompat;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;
import java.util.UUID;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;

/**
 * Created by weiyin on 8/8/14.
 */
public class Installation {
    private static final String TAG = Installation.class.getName();

    private static String sID = null;
    private static final String INSTALLATION = "INSTALLATION";

    public synchronized static String id(Context context) {
        if (sID == null) {
            File installation = new File(context.getFilesDir(), INSTALLATION);
            try {
                if (!installation.exists())
                    writeInstallationFile(installation);
                sID = readInstallationFile(installation);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return sID;
    }

    public static Map<String,Object> getInfo(Context context) {
        HashMap<String,Object> info = new HashMap<>();

        info.put("platform", "android");

        String publicKey = AppConfig.getInstance(context).publicKey;
        if (publicKey == null) publicKey = "";
        info.put("publicKey", publicKey);

        String packageName = context.getPackageName();
        info.put("appId", packageName);


        PackageManager manager = context.getPackageManager();
        try {
            PackageInfo packageInfo = manager.getPackageInfo(packageName, 0);
            info.put("appVersion", packageInfo.versionName);
            info.put("appVersionCode", packageInfo.versionCode);
        } catch (PackageManager.NameNotFoundException e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }

        String distribution;
        boolean isDebuggable =  ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );
        if (isDebuggable) {
            distribution = "debug";
        } else {
            String installer = manager.getInstallerPackageName(packageName);
            if (installer == null) {
                distribution = "adhoc";
            } else if (installer.equals("com.android.vending") || installer.equals("com.google.market")) {
                distribution = "playstore";
            } else if (installer.equals("com.amazon.venezia")) {
                distribution = "amazon";
            } else {
                distribution = installer;
            }
        }
        info.put("distribution", distribution);

        info.put("language", Locale.getDefault().getLanguage());
        info.put("os", "Android");
        info.put("osVersion", Build.VERSION.RELEASE);
        info.put("model", Build.MANUFACTURER + " " + Build.MODEL);
        info.put("hardware", Build.FINGERPRINT);
        info.put("timeZone", TimeZone.getDefault().getID());
        info.put("deviceName", getDeviceName());

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
            SubscriptionManager subscriptionManager = SubscriptionManager.from(context);

            if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {
                List<String> carriers = new ArrayList<>();
                for (SubscriptionInfo subscriptionInfo : subscriptionManager.getActiveSubscriptionInfoList()) {
                    carriers.add(subscriptionInfo.getCarrierName().toString());
                }
                info.put("carrierNames", carriers);
                try {
                    info.put("carrierName", carriers.get(0));
                } catch ( IndexOutOfBoundsException e ) {
                    Log.w(TAG, "getInfo: No carriers registered with subscription manager");
                }
            } else {
                Log.w(TAG, "getInfo: Cannot get carrierNames, READ_PHONE_STATE not granted");
            }
        }

        info.put("installationId", Installation.id(context));

        return info;
    }

    private static String readInstallationFile(File installation) throws IOException {
        RandomAccessFile f = new RandomAccessFile(installation, "r");
        byte[] bytes = new byte[(int) f.length()];
        f.readFully(bytes);
        f.close();
        return new String(bytes);
    }

    private static void writeInstallationFile(File installation) throws IOException {
        FileOutputStream out = new FileOutputStream(installation);
        String id = UUID.randomUUID().toString();
        out.write(id.getBytes());
        out.close();
    }

    private static String getDeviceName() {
        String manufacturer = Build.MANUFACTURER;
        String model = Build.MODEL;
        String name;
        if (model.startsWith(manufacturer)) {
            name = model;
        } else {
            name = manufacturer + " " + model;
        }
        return name;
    }
}

================================================
FILE: app/src/main/java/io/gonative/android/JsCustomCodeExecutor.java
================================================
package io.gonative.android;

import org.json.JSONException;
import org.json.JSONObject;

import java.util.Map;

import io.gonative.gonative_core.GNLog;

public class JsCustomCodeExecutor {
    private static final String TAG = JsCustomCodeExecutor.class.getName();

    public static interface CustomCodeHandler {
        JSONObject execute(Map<String, String> params);
    }

    // The default CustomCodeHandler "Echo"
    // Simply maps all the key/values of the given params into a JSONObject
    private static CustomCodeHandler handler = new CustomCodeHandler() {
        @Override
        public JSONObject execute(Map<String, String> params) {
            if(params != null) {
                JSONObject json = new JSONObject();
                try {
                    for(Map.Entry<String, String> entry : params.entrySet()) {
                        json.put(entry.getKey(), entry.getValue());
                    }
                }
                catch(JSONException e) {
                    GNLog.getInstance().logError(TAG, "Error building custom Json Data", e);
                }
                return json;
            }
            return null;
        }
    };

    /**
     * Set new CustomCodeHandler to override the default "Echo" handler
     * @param customHandler
     */
    public static void setHandler(CustomCodeHandler customHandler) {
        if(customHandler == null)
            return;
        handler = customHandler;
    }

    /**
     * Code Handler gets triggered by the UrlNavigation class
     *
     * @param params A map consisting of all URI parameters and their values
     * @return A JSONObject as defined by the Code Handler
     *
     * @see UrlNavigation#shouldOverrideUrlLoading
     */
    public static JSONObject execute(Map<String, String> params) {
        try {
            return handler.execute(params);
        } catch(Exception e) {
            GNLog.getInstance().logError(TAG, "Error executing custom code", e);
            return null;
        }
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/JsResultBridge.java
================================================
package io.gonative.android;

public class JsResultBridge {
    public static String jsResult = "";
}


================================================
FILE: app/src/main/java/io/gonative/android/JsonMenuAdapter.java
================================================
package io.gonative.android;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.StateListDrawable;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.localbroadcastmanager.content.LocalBroadcastManager;

import org.json.JSONArray;
import org.json.JSONObject;

import io.gonative.android.icons.Icon;
import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;

/**
 * Created by weiyin on 4/14/14.
 */
public class JsonMenuAdapter extends BaseExpandableListAdapter
        implements ExpandableListView.OnGroupClickListener, ExpandableListView.OnChildClickListener {
    private static final String TAG = JsonMenuAdapter.class.getName();

    private MainActivity mainActivity;
    private JSONArray menuItems;

    private boolean groupsHaveIcons = false;
    private boolean childrenHaveIcons = false;
    private String status;
    private int selectedIndex;
    private ExpandableListView expandableListView;
    private Integer highlightColor;
    private int sidebar_icon_size;
    private int sidebar_expand_indicator_size;

    JsonMenuAdapter(MainActivity activity, ExpandableListView expandableListView) {
        this.mainActivity = activity;
        sidebar_icon_size = mainActivity.getResources().getInteger(R.integer.sidebar_icon_size);
        sidebar_expand_indicator_size = mainActivity.getResources().getInteger(R.integer.sidebar_expand_indicator_size);
        this.expandableListView = expandableListView;
        menuItems = null;

        this.highlightColor = activity.getResources().getColor(R.color.sidebarHighlight);

        // broadcast messages
        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if (intent.getAction() != null && intent.getAction().equals(AppConfig.PROCESSED_MENU_MESSAGE)) {
                    update();
                }
            }
        };
        LocalBroadcastManager.getInstance(this.mainActivity)
                .registerReceiver(broadcastReceiver,
                        new IntentFilter(AppConfig.PROCESSED_MENU_MESSAGE));

    }

    private synchronized void update() {
        update(this.status);
    }

    public synchronized void update(String status) {
        if (status == null) status = "default";
        this.status = status;

        menuItems = AppConfig.getInstance(mainActivity).menus.get(status);
        if (menuItems == null) menuItems = new JSONArray();

        // figure out groupsHaveIcons and childrenHaveIcons (for layout alignment)
        groupsHaveIcons = false;
        childrenHaveIcons = false;
        for (int i = 0; i < menuItems.length(); i++) {
            JSONObject item = menuItems.optJSONObject(i);
            if (item == null) continue;

            if (!item.isNull("icon") && !item.optString("icon").isEmpty()) {
                groupsHaveIcons = true;
            }

            if (item.optBoolean("isGrouping", false)) {
                JSONArray sublinks = item.optJSONArray("subLinks");
                if (sublinks != null) {
                    for (int j = 0; j < sublinks.length(); j++) {
                        JSONObject sublink = sublinks.optJSONObject(j);
                        if (sublink != null && !sublink.isNull("icon") && !sublink.optString("icon").isEmpty()) {
                            childrenHaveIcons = true;
                            break;
                        }
                    }
                }

            }
        }

        notifyDataSetChanged();
    }


    private String itemString(String s, int groupPosition) {
        String value = null;
        try {
            JSONObject section = (JSONObject) menuItems.get(groupPosition);
            if (!section.isNull(s))
                value = section.getString(s).trim();
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }

        return value;
    }

    private String itemString(String s, int groupPosition, int childPosition) {
        String value = null;
        try {
            JSONObject section = (JSONObject) menuItems.get(groupPosition);
            JSONObject sublink = section.getJSONArray("subLinks").getJSONObject(childPosition);
            if (!sublink.isNull(s))
                value = sublink.getString(s).trim();
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }

        return value;
    }

    private String getTitle(int groupPosition) {
        return itemString("label", groupPosition);
    }

    private String getTitle(int groupPosition, int childPosition) {
        return itemString("label", groupPosition, childPosition);
    }

    private Pair<String, String> getUrlAndJavascript(int groupPosition) {
        String url = itemString("url", groupPosition);
        String js = itemString("javascript", groupPosition);
        return new Pair<>(url, js);
    }

    private Pair<String, String> getUrlAndJavascript(int groupPosition, int childPosition) {
        String url = itemString("url", groupPosition, childPosition);
        String js = itemString("javascript", groupPosition, childPosition);
        return new Pair<>(url, js);
    }

    private boolean isGrouping(int groupPosition) {
        try {
            JSONObject section = (JSONObject) menuItems.get(groupPosition);
            return section.optBoolean("isGrouping", false);
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
            return false;
        }
    }

    @Override
    public int getGroupCount() {
        return menuItems.length();
    }

    @Override
    public int getChildrenCount(int groupPosition) {
        int count = 0;
        try {
            JSONObject section = (JSONObject) menuItems.get(groupPosition);
            if (section.optBoolean("isGrouping", false)) {
                count = section.getJSONArray("subLinks").length();
            } else {
                count = 0;
            }
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }
        return count;
    }

    @Override
    public Object getGroup(int i) {
        return null;
    }

    @Override
    public Object getChild(int i, int i2) {
        return null;
    }

    @Override
    public long getGroupId(int i) {
        return 0;
    }

    @Override
    public long getChildId(int i, int i2) {
        return 0;
    }

    @Override
    public boolean hasStableIds() {
        return false;
    }

    @Override
    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
        if (convertView == null) {
            LayoutInflater inflater = mainActivity.getLayoutInflater();

            convertView = inflater.inflate(groupsHaveIcons ?
                    R.layout.menu_group_icon : R.layout.menu_group_noicon, null);


            TextView title = convertView.findViewById(R.id.menu_item_title);
            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));

        }
        RelativeLayout menuItem = convertView.findViewById(R.id.menu_item);
        GradientDrawable shape = getHighlightDrawable();
        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(new int[]{android.R.attr.state_activated}, shape);
        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, shape);

        menuItem.setBackground(stateListDrawable);

        // expand/collapse indicator
        ImageView indicator = convertView.findViewById(R.id.menu_group_indicator);
        if (isGrouping(groupPosition)) {
            String iconName;
            int color = Color.BLACK;
            if (isExpanded) {
                iconName = "fas fa-angle-up";
            } else {
                iconName = "fas fa-angle-down";
            }

            if (groupPosition == this.selectedIndex) {
                color = this.highlightColor;
            } else {
                color = mainActivity.getResources().getColor(R.color.sidebarForeground);
            }
            indicator.setImageDrawable(new Icon(mainActivity, iconName, sidebar_expand_indicator_size, color).getDrawable());

            indicator.setVisibility(View.VISIBLE);
        } else {
            indicator.setVisibility(View.GONE);
        }

        //set the title
        TextView title = convertView.findViewById(R.id.menu_item_title);
        title.setText(getTitle(groupPosition));
        if (this.selectedIndex == groupPosition) {
            title.setTextColor(this.highlightColor);
        } else {
            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));
        }

        // set icon
        String icon = itemString("icon", groupPosition);
        ImageView imageView = convertView.findViewById(R.id.menu_item_icon);
        if (icon != null && !icon.isEmpty()) {
            int color;
            if (groupPosition == this.selectedIndex) {
                color = this.highlightColor;
            } else {
                color = mainActivity.getResources().getColor(R.color.sidebarForeground);
            }
            Drawable iconDrawable = new Icon(mainActivity, icon, sidebar_icon_size, color).getDrawable();

            imageView.setImageDrawable(iconDrawable);
            imageView.setVisibility(View.VISIBLE);
        } else if (imageView != null) {
            imageView.setVisibility(View.INVISIBLE);
        }

        return convertView;
    }

    @Override
    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
                             View convertView, ViewGroup parent) {

        if (convertView == null) {
            LayoutInflater inflater = mainActivity.getLayoutInflater();

            if (groupsHaveIcons || childrenHaveIcons)
                convertView = inflater.inflate(R.layout.menu_child_icon, parent, false);
            else
                convertView = inflater.inflate(R.layout.menu_child_noicon, parent, false);

            // style it
            TextView title = convertView.findViewById(R.id.menu_item_title);
            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));
        }

        RelativeLayout menuItem = convertView.findViewById(R.id.menu_item);
        GradientDrawable shape = getHighlightDrawable();
        StateListDrawable stateListDrawable = new StateListDrawable();
        stateListDrawable.addState(new int[]{android.R.attr.state_activated}, shape);
        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, shape);

        menuItem.setBackground(stateListDrawable);

        // set title
        TextView title = convertView.findViewById(R.id.menu_item_title);
        title.setText(getTitle(groupPosition, childPosition));
        if (this.selectedIndex == (groupPosition + childPosition) + 1) {
            title.setTextColor(this.highlightColor);
        } else {
            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));
        }

        // set icon
        String icon = itemString("icon", groupPosition, childPosition);
        ImageView imageView = convertView.findViewById(R.id.menu_item_icon);
        if (icon != null && !icon.isEmpty()) {
            int color;
            if (this.selectedIndex == (groupPosition + childPosition) + 1) {
                color = this.highlightColor;
            } else {
                color = mainActivity.getResources().getColor(R.color.sidebarForeground);
            }
            Drawable iconDrawable = new Icon(mainActivity, icon, sidebar_icon_size, color).getDrawable();

            imageView.setImageDrawable(iconDrawable);
            imageView.setVisibility(View.VISIBLE);
        } else if (imageView != null) {
            imageView.setVisibility(View.INVISIBLE);
        }

        return convertView;
    }

    @Override
    public boolean isChildSelectable(int groupPosition, int childPosition) {
        return true;
    }

    @Override
    public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
        try {
            if (isGrouping(groupPosition)) {
                // return false for default handling behavior
                return false;
            } else {
                Pair<String,String> urlAndJavascript = getUrlAndJavascript(groupPosition);
                loadUrlAndJavascript(urlAndJavascript.first, urlAndJavascript.second);
                return true; // tell android that we have handled it
            }
        } catch (Exception e) {
            GNLog.getInstance().logError(TAG, e.getMessage(), e);
        }

        return false;
    }

    @Override
    public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
        int index = parent.getFlatListPosition(ExpandableListView.getPackedPositionForChild(groupPosition, childPosition));
        parent.setItemChecked(index, true);
        this.selectedIndex = index;
        Pair<String, String> urlAndJavascript = getUrlAndJavascript(groupPosition, childPosition);
        loadUrlAndJavascript(urlAndJavascript.first, urlAndJavascript.second);
        return true;
    }

    private void loadUrlAndJavascript(String url, String javascript) {
        // check for GONATIVE_USERID
        if (UrlInspector.getInstance().getUserId() != null) {
            url = url.replaceAll("GONATIVE_USERID", UrlInspector.getInstance().getUserId());
        }

        if (javascript == null) mainActivity.loadUrl(url);
        else mainActivity.loadUrlAndJavascript(url, javascript);

        mainActivity.closeDrawers();
    }

    public void autoSelectItem(String url) {
        String formattedUrl = url.replaceAll("/$", "");
        if (menuItems == null) return;

        for (int i = 0; i < menuItems.length(); i++) {
            if (formattedUrl.equals(menuItems.optJSONObject(i).optString("url").replaceAll("/$", ""))) {
                expandableListView.setItemChecked(i, true);
                selectedIndex = i;
                return;
            }
        }
    }

    private GradientDrawable getHighlightDrawable() {
        GradientDrawable shape = new GradientDrawable();
        shape.setCornerRadius(10);
        shape.setColor(this.highlightColor);
        shape.setAlpha(30);

        return shape;
    }

    @Override
    public int getChildType(int groupPosition, int childPosition) {
        if (groupsHaveIcons || childrenHaveIcons) return 0;
        else return 1;
    }

    @Override
    public int getChildTypeCount() {
        return 2;
    }

    @Override
    public int getGroupType(int groupPosition) {
        if (groupsHaveIcons) return 0;
        else return 1;
    }

    @Override
    public int getGroupTypeCount() {
        return 2;
    }
}


================================================
FILE: app/src/main/java/io/gonative/android/KeyboardManager.kt
================================================
package io.gonative.android

import android.graphics.Rect
import android.text.TextUtils
import android.view.ViewGroup
import io.gonative.gonative_core.LeanUtils
import org.json.JSONObject


class KeyboardManager(val activity: MainActivity, private val rootLayout: ViewGroup) {

    var callback: String? = ""
    private var keyboardWidth = 0
    private var keyboardHeight = 0
    private var screenWidth = 0
    private var screenHeight = 0
    private var isKeyboardVisible = false
    private var screenHeightOffset = 0

    init {
        rootLayout.viewTreeObserver
            .addOnGlobalLayoutListener {
                val r = Rect()
                rootLayout.getWindowVisibleDisplayFrame(r)

                if (screenHeightOffset == 0) {
                    screenHeightOffset = rootLayout.rootView.height - r.bottom
                }

                screenWidth = rootLayout.rootView.width
                screenHeight = r.bottom + screenHeightOffset

                keyboardHeight = rootLayout.rootView.height - screenHeight

                if (keyboardHeight == screenHeightOffset) {
                    keyboardHeight = 0
                }

                val visible =  keyboardHeight != 0

                if (visible) {
                    keyboardWidth = screenWidth
                    if (!isKeyboardVisible) {
                        isKeyboardVisible = true
                        notifyCallback();
                    }
                } else {
                    keyboardWidth = 0
                    if (isKeyboardVisible) {
                        isKeyboardVisible = false
                        notifyCallback();
                    }
                }
            }
    }

    private fun notifyCallback() {
        if (TextUtils.isEmpty(callback)) return
        activity.runJavascript(LeanUtils.createJsForCallback(callback, getKeyboardData()))
    }

    fun getKeyboardData() : JSONObject {
        val keyboardWindowSize = JSONObject()
        keyboardWindowSize.put("visible", isKeyboardVisible)
        keyboardWindowSize.put("width", keyboardWidth)
        keyboardWindowSize.put("height", keyboardHeight)

        val visibleWindowSize = JSONObject()
        visibleWindowSize.put("width", screenWidth)
        visibleWindowSize.put("height", screenHeight)

        val data = JSONObject()
        data.put("keyboardWindowSize", keyboardWindowSize)
        data.put("visibleWindowSize", visibleWindowSize)

        return data
    }
}

================================================
FILE: app/src/main/java/io/gonative/android/LoginManager.java
================================================
package io.gonative.android;

import android.content.Context;
import android.os.AsyncTask;

import org.json.JSONObject;

import java.lang.ref.WeakReference;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;
import java.util.Observable;
import java.util.regex.Pattern;

import io.gonative.gonative_core.AppConfig;
import io.gonative.gonative_core.GNLog;

/**
 * Created by weiyin on 3/16/14.
 */
public class LoginManager extends Observable {
    private static final String TAG = LoginManager.class.getName();

    private Context context;
    private CheckRedirectionTask task = null;

    private boolean loggedIn = false;

    LoginManager(Context context) {
        this.context = context;
        checkLogin();
    }

    public void checkLogin() {
        if (task != null)
            task.cancel(true);

        String loginDetectionUrl = AppConfig.getInstance(context).loginDetectionUrl;
        if (loginDetectionUrl == null) {
            return;
        }

        task = new CheckRedirectionTask(this);
        task.execute(AppConfig.getInstance(context).loginDetectionUrl);
    }

    public boolean isLoggedIn() {
        return loggedIn;
    }


    private static class CheckRedirectionTask extends AsyncTask<String, Void, String> {
        private WeakReference<LoginManager> loginManagerReference;

        public CheckRedirectionTask(LoginManager loginManager) {
            this.loginManagerReference = new WeakReference<>(loginManager);
        }

        @Override
        protected String doInBackground(String... urls){
            LoginManager loginManager = loginManagerReference.get();
            if (loginManager == null) return null;

            try {
                URL parsedUrl = new URL(urls[0]);
                HttpURLConnection connection = null;
                boolean wasRedirected;
                int numRedirects = 0;
                do {
                    if (connection != null)
                        connection.disconnect();

                    connection = (HttpURLConnection) parsedUrl.openConnection();
                    connection.setInstanceFollowRedirects(true);
                    connection.setRequestProperty("User-Agent", AppConfig.getInstance(loginManager.context).userAgent);

                    connection.connect();
                    int responseCode = connection.getResponseCode();

                    if (responseCode == HttpURLConnection.HTTP_MOVED_PERM ||
                            responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {
                        wasRedirected = true;
                        parsedUrl = new URL(parsedUrl, connection.getHeaderField("Location"));
                        numRedirects++;
                    } else {
                        wasRedirected = false;
                    }
                } while (!isCancelled() && wasRedirected && numRedirects < 10);

                String finalUrl = connection.getURL().toString();
                connection.disconnect();
                return finalUrl;

            } catch (Exception e) {
                GNLog.getInstance().logError(TAG, e.getMessage(), e);
                return null;
            }
        }

        @Override
        protected void onPostExecute(String finalUrl) {
            LoginManager loginManager = loginManagerReference.get();
            if (loginManager == null) return;

            UrlInspector.getInstance().inspectUrl(finalUrl);
            String loginStatus;

            if (finalUrl == null) {
                loginManager.loggedIn = false;
                loginStatus = "default";
                loginManager.setChanged();
                loginManager.notifyObservers();
                return;
            }

            // iterate through loginDetectionRegexes
            AppConfig appConfig = AppConfig.getInstance(loginManager.context);

            List<Pattern> regexes = appConfig.loginDetectRegexes;
            for (int i = 0; i < regexes.size(); i++) {
                Pattern regex = regexes.get(i);
                if (regex.matcher(finalUrl).matches()) {
                    JSONObject entry = appConfig.loginDetectLocations.get(i);
                    loginManager.loggedIn = entry.optBoolean("loggedIn", false);

                    loginStatus = AppConfig.optString(entry, "menuName");
                    if (loginStatus == null) loginStatus = loginManager.loggedIn ? "loggedIn" : "default";

                    loginManager.setChanged();
                    loginManager.notifyObservers();
                    break;
                }
            }
        }

    }
}


================================================
FILE: app/src/main/java/io/gonative/android/MainActivity.java
================================================
package io.gonative.android;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Broadcast
Download .txt
gitextract_20zoy71i/

├── .github/
│   └── workflows/
│       └── firebase-testing.yml
├── .gitignore
├── AppIcon
├── CHANGELOG.md
├── HeaderImage
├── NotificationIcon
├── README.md
├── app/
│   ├── .gitignore
│   ├── build.gradle
│   ├── proguard-project.txt
│   └── src/
│       ├── androidTest/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   ├── HelperClass.java
│       │   │   └── appConfig.json
│       │   └── java/
│       │       └── com/
│       │           └── gonative/
│       │               └── testFiles/
│       │                   ├── FirstTestClass.java
│       │                   └── TestMethods.java
│       ├── main/
│       │   ├── AndroidManifest.xml
│       │   ├── assets/
│       │   │   ├── BlobDownloader.js
│       │   │   ├── GoNativeJSBridgeLibrary.js
│       │   │   ├── appConfig.json
│       │   │   ├── custom-icons.json
│       │   │   └── offline.html
│       │   ├── java/
│       │   │   └── io/
│       │   │       └── gonative/
│       │   │           └── android/
│       │   │               ├── ActionManager.java
│       │   │               ├── AppLinksActivity.java
│       │   │               ├── AudioUtils.java
│       │   │               ├── ConfigPreferences.java
│       │   │               ├── ConfigUpdater.java
│       │   │               ├── CustomHeaders.java
│       │   │               ├── DownloadService.java
│       │   │               ├── FileDownloader.java
│       │   │               ├── FileUploadIntentsCreator.kt
│       │   │               ├── FileWriterSharer.java
│       │   │               ├── GoNativeApplication.java
│       │   │               ├── GoNativeWindowManager.java
│       │   │               ├── HtmlIntercept.java
│       │   │               ├── IOUtils.java
│       │   │               ├── Installation.java
│       │   │               ├── JsCustomCodeExecutor.java
│       │   │               ├── JsResultBridge.java
│       │   │               ├── JsonMenuAdapter.java
│       │   │               ├── KeyboardManager.kt
│       │   │               ├── LoginManager.java
│       │   │               ├── MainActivity.java
│       │   │               ├── MySwipeRefreshLayout.java
│       │   │               ├── ProfilePicker.java
│       │   │               ├── RegistrationManager.java
│       │   │               ├── SegmentedController.java
│       │   │               ├── ShakeDialogFragment.java
│       │   │               ├── SplashActivity.java
│       │   │               ├── TabManager.java
│       │   │               ├── UrlInspector.java
│       │   │               ├── UrlNavigation.java
│       │   │               ├── WebViewPool.java
│       │   │               ├── WebViewPoolDisownPolicy.java
│       │   │               ├── files/
│       │   │               │   └── CapturedImageSaver.kt
│       │   │               └── widget/
│       │   │                   ├── CircleImageView.java
│       │   │                   ├── GoNativeDrawerLayout.java
│       │   │                   ├── GoNativeSwipeRefreshLayout.java
│       │   │                   ├── HandleView.kt
│       │   │                   ├── SwipeHistoryNavigationLayout.kt
│       │   │                   └── WebViewContainerView.java
│       │   └── res/
│       │       ├── anim/
│       │       │   └── fast_fade_out.xml
│       │       ├── drawable/
│       │       │   ├── bg_nav_icon.xml
│       │       │   ├── ic_baseline_arrow_back_24.xml
│       │       │   ├── ic_baseline_arrow_forward_24.xml
│       │       │   ├── ic_go_back.xml
│       │       │   ├── ic_go_forward.xml
│       │       │   ├── ic_stat_onesignal_default.xml
│       │       │   └── shape_rounded.xml
│       │       ├── layout/
│       │       │   ├── actionbar_title.xml
│       │       │   ├── activity_gonative.xml
│       │       │   ├── activity_subscriptions.xml
│       │       │   ├── button_menu.xml
│       │       │   ├── empty.xml
│       │       │   ├── menu_child_icon.xml
│       │       │   ├── menu_child_noicon.xml
│       │       │   ├── menu_group_icon.xml
│       │       │   ├── menu_group_noicon.xml
│       │       │   ├── profile_picker_dropdown.xml
│       │       │   ├── splash_screen.xml
│       │       │   ├── tab.xml
│       │       │   └── view_handle.xml
│       │       ├── menu/
│       │       │   └── topmenu.xml
│       │       ├── mipmap-anydpi-v26/
│       │       │   ├── ic_launcher.xml
│       │       │   └── ic_launcher_round.xml
│       │       ├── values/
│       │       │   ├── attrs.xml
│       │       │   ├── colors.xml
│       │       │   ├── dimens.xml
│       │       │   ├── ic_launcher_background.xml
│       │       │   ├── integers.xml
│       │       │   ├── strings.xml
│       │       │   └── styles.xml
│       │       ├── values-ko/
│       │       │   └── strings.xml
│       │       ├── values-large/
│       │       │   └── styles.xml
│       │       ├── values-night/
│       │       │   ├── colors.xml
│       │       │   └── styles.xml
│       │       ├── values-night-v29/
│       │       │   └── styles.xml
│       │       ├── values-sw600dp/
│       │       │   ├── attr.xml
│       │       │   └── dimens.xml
│       │       ├── values-sw720dp-land/
│       │       │   └── dimens.xml
│       │       ├── values-v21/
│       │       │   └── styles.xml
│       │       ├── values-v29/
│       │       │   └── styles.xml
│       │       └── xml/
│       │           ├── filepaths.xml
│       │           └── network_security_config.xml
│       └── normal/
│           └── java/
│               └── io/
│                   └── gonative/
│                       └── android/
│                           ├── GoNativeWebChromeClient.java
│                           ├── GoNativeWebviewClient.java
│                           ├── LeanWebView.java
│                           ├── PoolWebViewClient.java
│                           ├── WebViewSetup.java
│                           └── WebkitCookieManagerProxy.java
├── build.gradle
├── generate-app-icons.sh
├── generate-header-images.sh
├── generate-plugin-icons.sh
├── generate-theme.js
├── generate-tinted-icons.sh
├── gradle/
│   └── wrapper/
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── plugins.gradle
└── settings.gradle
Download .txt
SYMBOL INDEX (685 symbols across 46 files)

FILE: app/src/androidTest/assets/HelperClass.java
  class HelperClass (line 3) | public class HelperClass {

FILE: app/src/androidTest/java/com/gonative/testFiles/FirstTestClass.java
  class FirstTestClass (line 19) | @RunWith(AndroidJUnit4.class)
    method initMethod (line 31) | @Before
    method testSidebarNavigation (line 51) | @Test
    method testTabMenuNavigation (line 63) | @Test
    method testIvE (line 73) | @Test
    method pullToRefresh (line 80) | @Test
    method testSearch (line 89) | @Test
    method testRefreshButton (line 98) | @Test

FILE: app/src/androidTest/java/com/gonative/testFiles/TestMethods.java
  class TestMethods (line 36) | public class TestMethods {
    method TestMethods (line 38) | public TestMethods(MainActivity mainActivity, WebView webView){
    method m_UpdateCurrentURL (line 48) | private void m_UpdateCurrentURL() throws InterruptedException {
    method getNewLoad (line 53) | protected int getNewLoad(){
    method isURL (line 57) | public boolean isURL(String url){
    method waitForPageLoaded (line 66) | public void waitForPageLoaded() throws InterruptedException {
    method testNavigation (line 85) | public void testNavigation(JSONArray sidebarObjects) throws Interrupte...
    method testInternalvExternalLinks (line 95) | public void testInternalvExternalLinks(UiDevice uiDevice) throws Inter...
    method testRefreshButton (line 117) | public void testRefreshButton() throws InterruptedException {
    method testSearchButton (line 122) | public void testSearchButton() throws InterruptedException {
    method m_testTabNavigation (line 137) | public void m_testTabNavigation(HashMap<String, JSONArray> tabMenus, A...
    method testPullToRefresh (line 179) | public void testPullToRefresh() throws InterruptedException {

FILE: app/src/main/assets/BlobDownloader.js
  function gonativeDownloadBlobUrl (line 3) | function gonativeDownloadBlobUrl(url) {
  function gonativeGotStoragePermissions (line 72) | function gonativeGotStoragePermissions() {

FILE: app/src/main/assets/GoNativeJSBridgeLibrary.js
  function addCommandCallback (line 3) | function addCommandCallback(command, params, persistCallback) {
  function addCallbackFunction (line 24) | function addCallbackFunction(callbackFunction, persistCallback){
  function addCommand (line 40) | function addCommand(command, params, persistCallback){
  function gonative_match_statusbar_to_body_background_color (line 271) | function gonative_match_statusbar_to_body_background_color() {

FILE: app/src/main/java/io/gonative/android/ActionManager.java
  class ActionManager (line 45) | public class ActionManager {
    method ActionManager (line 72) | ActionManager(MainActivity activity) {
    method setupActionBar (line 85) | public void setupActionBar(boolean isRoot) {
    method setupTitleDisplayForUrl (line 123) | public void setupTitleDisplayForUrl(String url) {
    method showLogoInActionBar (line 175) | private void showLogoInActionBar(boolean show) {
    method showTextActionBarTitle (line 185) | public void showTextActionBarTitle(CharSequence title) {
    method showTitleView (line 196) | public void showTitleView(View titleView) {
    method cleanSidebarMenuTitleOffset (line 216) | public void cleanSidebarMenuTitleOffset() {
    method checkActions (line 221) | public void checkActions(String url) {
    method setMenuID (line 244) | private void setMenuID(String menuID) {
    method addActions (line 259) | public void addActions(Menu menu) {
    method addLeftButton (line 291) | private void addLeftButton(AppConfig appConfig, JSONObject entry) {
    method addLeftCustomButton (line 334) | private void addLeftCustomButton(String icon, String url) {
    method addRightButton (line 340) | private void addRightButton(AppConfig appConfig, Menu menu, int itemID...
    method addRightCustomButton (line 405) | private void addRightCustomButton(Menu menu, int itemID, String label,...
    method replaceLeftIcon (line 419) | private void replaceLeftIcon(View view) {
    method createButtonMenu (line 430) | private Button createButtonMenu(String iconString) {
    method createSearchView (line 440) | private SearchView createSearchView(AppConfig appConfig, String icon, ...
    method setupActionBarDisplay (line 567) | public void setupActionBarDisplay() {
    method isOnSearchMode (line 610) | public boolean isOnSearchMode() {
    method setOnSearchMode (line 614) | public void setOnSearchMode(boolean onSearchMode) {
    method closeSearchView (line 618) | public void closeSearchView() {
    method onOptionsItemSelected (line 626) | public boolean onOptionsItemSelected(MenuItem item) {

FILE: app/src/main/java/io/gonative/android/AppLinksActivity.java
  class AppLinksActivity (line 9) | public class AppLinksActivity extends AppCompatActivity {
    method onCreate (line 11) | @Override
    method launchApp (line 17) | private void launchApp() {

FILE: app/src/main/java/io/gonative/android/AudioUtils.java
  class AudioUtils (line 9) | public class AudioUtils {
    method setUpAudioDevice (line 22) | public static void setUpAudioDevice(MainActivity mainActivity, int mod...
    method reconnectToBluetooth (line 44) | public static void reconnectToBluetooth(MainActivity mainActivity, Aud...
    method initAudioFocusListener (line 56) | public static void initAudioFocusListener(MainActivity mainActivity) {
    method requestAudioFocus (line 105) | public static void requestAudioFocus(MainActivity mainActivity) {
    method abandonFocusRequest (line 158) | public static void abandonFocusRequest(MainActivity mainActivity) {

FILE: app/src/main/java/io/gonative/android/ConfigPreferences.java
  class ConfigPreferences (line 8) | public class ConfigPreferences {
    method ConfigPreferences (line 14) | public ConfigPreferences(Context context) {
    method getSharedPreferences (line 18) | private SharedPreferences getSharedPreferences() {
    method getAppTheme (line 25) | public String getAppTheme() {
    method setAppTheme (line 30) | public void setAppTheme(String appTheme) {

FILE: app/src/main/java/io/gonative/android/ConfigUpdater.java
  class ConfigUpdater (line 20) | public class ConfigUpdater {
    method ConfigUpdater (line 25) | ConfigUpdater(Context context) {
    method registerEvent (line 29) | public void registerEvent() {
    class EventTask (line 35) | private static class EventTask extends AsyncTask<Void, Void, Void> {
      method EventTask (line 38) | EventTask(Context context) {
      method doInBackground (line 42) | @Override

FILE: app/src/main/java/io/gonative/android/CustomHeaders.java
  class CustomHeaders (line 19) | public class CustomHeaders {
    method getCustomHeaders (line 20) | public static Map<String, String> getCustomHeaders(Context context) {
    method interpolateValues (line 42) | private static String interpolateValues(Context context, String value)...

FILE: app/src/main/java/io/gonative/android/DownloadService.java
  class DownloadService (line 36) | public class DownloadService extends Service {
    method onCreate (line 49) | @Override
    method onStartCommand (line 56) | @Override
    method onBind (line 65) | @Nullable
    class DownloadBinder (line 71) | public class DownloadBinder extends Binder {
      method getService (line 72) | public DownloadService getService() {
    method startDownload (line 77) | public void startDownload(String url, String filename, String mimetype...
    method cancelDownload (line 83) | public void cancelDownload(int downloadId) {
    method handleDownloadUri (line 90) | public void handleDownloadUri(FileDownloader.DownloadLocation location...
    method viewFile (line 118) | private void viewFile(Uri uri, String mimeType) {
    method addFileToGallery (line 134) | private void addFileToGallery(Uri uri) {
    class DownloadTask (line 141) | private class DownloadTask {
      method DownloadTask (line 157) | public DownloadTask(String url, String filename, String mimetype, bo...
      method getId (line 168) | public int getId() {
      method isDownloading (line 172) | public boolean isDownloading() {
      method startDownload (line 176) | public void startDownload() {
      method cancelDownload (line 305) | public void cancelDownload() {

FILE: app/src/main/java/io/gonative/android/FileDownloader.java
  class FileDownloader (line 37) | public class FileDownloader implements DownloadListener {
    type DownloadLocation (line 38) | public enum DownloadLocation {
    method onServiceConnected (line 53) | @Override
    method onServiceDisconnected (line 60) | @Override
    method FileDownloader (line 67) | FileDownloader(MainActivity context) {
    method onDownloadStart (line 99) | @Override
    method downloadFile (line 150) | public void downloadFile(String url, String filename, boolean shouldSa...
    method startDownload (line 182) | private void startDownload(String downloadUrl, String filename, String...
    method requestRequiredPermission (line 190) | private boolean requestRequiredPermission(PreDownloadInfo preDownloadI...
    method getLastDownloadedUrl (line 208) | public String getLastDownloadedUrl() {
    method setUrlNavigation (line 212) | public void setUrlNavigation(UrlNavigation urlNavigation) {
    method unbindDownloadService (line 216) | public void unbindDownloadService() {
    method createExternalFileUri (line 223) | public static Uri createExternalFileUri(ContentResolver contentResolve...
    method createOutputFile (line 238) | public static File createOutputFile(File dir, String filename, String ...
    method getUniqueFileName (line 242) | public static String getUniqueFileName(String fileName, File dir) {
    method getFilenameExtension (line 264) | public static String getFilenameExtension(String name) {
    class PreDownloadInfo (line 275) | private static class PreDownloadInfo {
      method PreDownloadInfo (line 283) | public PreDownloadInfo(String url, String filename, String mimetype,...
      method PreDownloadInfo (line 292) | public PreDownloadInfo(String url, String filename, boolean isBlob, ...

FILE: app/src/main/java/io/gonative/android/FileWriterSharer.java
  class FileWriterSharer (line 38) | public class FileWriterSharer {
    class FileInfo (line 46) | private static class FileInfo{
    class JavascriptBridge (line 58) | private class JavascriptBridge {
      method postMessage (line 59) | @JavascriptInterface
    method FileWriterSharer (line 89) | public FileWriterSharer(MainActivity context) {
    method getJavascriptBridge (line 102) | public JavascriptBridge getJavascriptBridge() {
    method downloadBlobUrl (line 106) | public void downloadBlobUrl(String url, String filename, boolean open) {
    method onFileStart (line 127) | private void onFileStart(JSONObject message) throws IOException {
    method onFileStartAfterPermission (line 206) | private void onFileStartAfterPermission(FileInfo info, boolean granted...
    method onFileChunk (line 227) | private void onFileChunk(JSONObject message) throws IOException {
    method onFileEnd (line 268) | private void onFileEnd(JSONObject message) throws IOException {
    method getIntentToOpenFile (line 306) | private Intent getIntentToOpenFile(Uri uri, String mimetype) {
    method onNextFileInfo (line 313) | private void onNextFileInfo(JSONObject message) {

FILE: app/src/main/java/io/gonative/android/GoNativeApplication.java
  class GoNativeApplication (line 22) | public class GoNativeApplication extends MultiDexApplication {
    method getPlugins (line 33) | @Override
    method onCreate (line 43) | @Override
    method getLoginManager (line 71) | public LoginManager getLoginManager() {
    method getRegistrationManager (line 75) | public RegistrationManager getRegistrationManager() {
    method getWebViewPool (line 79) | public WebViewPool getWebViewPool() {
    method getWebviewMessage (line 83) | public Message getWebviewMessage() {
    method setWebviewMessage (line 87) | public void setWebviewMessage(Message webviewMessage) {
    method getAnalyticsProviderInfo (line 91) | public Map<String, Object> getAnalyticsProviderInfo() {
    method getWebviewValueCallback (line 96) | @SuppressWarnings("unused")
    method setWebviewValueCallback (line 101) | public void setWebviewValueCallback(ValueCallback webviewValueCallback) {
    method getWindowManager (line 105) | public GoNativeWindowManager getWindowManager() {

FILE: app/src/main/java/io/gonative/android/GoNativeWindowManager.java
  class GoNativeWindowManager (line 8) | public class GoNativeWindowManager {
    method GoNativeWindowManager (line 12) | public GoNativeWindowManager() {
    method addNewWindow (line 16) | public void addNewWindow(String activityId, boolean isRoot) {
    method removeWindow (line 20) | public void removeWindow(String activityId) {
    method setOnExcessWindowClosedListener (line 28) | public void setOnExcessWindowClosedListener(ExcessWindowsClosedListene...
    method getActivityWindowInfo (line 32) | public ActivityWindow getActivityWindowInfo(String activityId) {
    method setUrlLevel (line 36) | public void setUrlLevel(String activityId, int urlLevel) {
    method getUrlLevel (line 43) | public int getUrlLevel(String activityId) {
    method setParentUrlLevel (line 51) | public void setParentUrlLevel(String activityId, int parentLevel) {
    method getParentUrlLevel (line 58) | public int getParentUrlLevel(String activityId) {
    method setUrlLevels (line 66) | public void setUrlLevels(String activityId, int urlLevel, int parentLe...
    method isRoot (line 73) | public boolean isRoot(String activityId) {
    method setAsNewRoot (line 81) | public void setAsNewRoot(String activityId) {
    method setIgnoreInterceptMaxWindows (line 92) | public void setIgnoreInterceptMaxWindows(String activityId, boolean ig...
    method isIgnoreInterceptMaxWindows (line 99) | public boolean isIgnoreInterceptMaxWindows(String activityId) {
    method getWindowCount (line 107) | public int getWindowCount() {
    method getExcessWindow (line 112) | public String getExcessWindow() {
    class ActivityWindow (line 121) | public static class ActivityWindow {
      method ActivityWindow (line 128) | ActivityWindow(String id, boolean isRoot) {
      method setUrlLevels (line 135) | public void setUrlLevels(int urlLevel, int parentUrlLevel) {
      method toString (line 140) | @Override
    type ExcessWindowsClosedListener (line 150) | interface ExcessWindowsClosedListener {
      method onAllExcessWindowClosed (line 151) | void onAllExcessWindowClosed();

FILE: app/src/main/java/io/gonative/android/HtmlIntercept.java
  class HtmlIntercept (line 29) | public class HtmlIntercept {
    method HtmlIntercept (line 41) | HtmlIntercept(Context context) {
    method setInterceptUrl (line 45) | public void setInterceptUrl(String interceptUrl) {
    method interceptHtml (line 49) | public WebResourceResponse interceptHtml(GoNativeWebviewInterface view...
    method urlMatches (line 232) | private static boolean urlMatches(String url1, String url2) {
    method stringsNotEqual (line 264) | private static boolean stringsNotEqual(String s1, String s2) {
    method getCharset (line 268) | private static String getCharset(String contentType) {
    method getRedirectedUrl (line 283) | public String getRedirectedUrl() {
    method setRedirectedUrl (line 287) | public void setRedirectedUrl(String redirectUrl) {

FILE: app/src/main/java/io/gonative/android/IOUtils.java
  class IOUtils (line 11) | public class IOUtils {
    method copy (line 14) | public static void copy(InputStream in, OutputStream out) throws IOExc...
    method close (line 22) | public static void close(Closeable c) {

FILE: app/src/main/java/io/gonative/android/Installation.java
  class Installation (line 32) | public class Installation {
    method id (line 38) | public synchronized static String id(Context context) {
    method getInfo (line 52) | public static Map<String,Object> getInfo(Context context) {
    method readInstallationFile (line 124) | private static String readInstallationFile(File installation) throws I...
    method writeInstallationFile (line 132) | private static void writeInstallationFile(File installation) throws IO...
    method getDeviceName (line 139) | private static String getDeviceName() {

FILE: app/src/main/java/io/gonative/android/JsCustomCodeExecutor.java
  class JsCustomCodeExecutor (line 10) | public class JsCustomCodeExecutor {
    type CustomCodeHandler (line 13) | public static interface CustomCodeHandler {
      method execute (line 14) | JSONObject execute(Map<String, String> params);
    method execute (line 20) | @Override
    method setHandler (line 42) | public static void setHandler(CustomCodeHandler customHandler) {
    method execute (line 56) | public static JSONObject execute(Map<String, String> params) {

FILE: app/src/main/java/io/gonative/android/JsResultBridge.java
  class JsResultBridge (line 3) | public class JsResultBridge {

FILE: app/src/main/java/io/gonative/android/JsonMenuAdapter.java
  class JsonMenuAdapter (line 33) | public class JsonMenuAdapter extends BaseExpandableListAdapter
    method JsonMenuAdapter (line 49) | JsonMenuAdapter(MainActivity activity, ExpandableListView expandableLi...
    method update (line 73) | private synchronized void update() {
    method update (line 77) | public synchronized void update(String status) {
    method itemString (line 114) | private String itemString(String s, int groupPosition) {
    method itemString (line 127) | private String itemString(String s, int groupPosition, int childPositi...
    method getTitle (line 141) | private String getTitle(int groupPosition) {
    method getTitle (line 145) | private String getTitle(int groupPosition, int childPosition) {
    method getUrlAndJavascript (line 149) | private Pair<String, String> getUrlAndJavascript(int groupPosition) {
    method getUrlAndJavascript (line 155) | private Pair<String, String> getUrlAndJavascript(int groupPosition, in...
    method isGrouping (line 161) | private boolean isGrouping(int groupPosition) {
    method getGroupCount (line 171) | @Override
    method getChildrenCount (line 176) | @Override
    method getGroup (line 192) | @Override
    method getChild (line 197) | @Override
    method getGroupId (line 202) | @Override
    method getChildId (line 207) | @Override
    method hasStableIds (line 212) | @Override
    method getGroupView (line 217) | @Override
    method getChildView (line 291) | @Override
    method isChildSelectable (line 346) | @Override
    method onGroupClick (line 351) | @Override
    method onChildClick (line 369) | @Override
    method loadUrlAndJavascript (line 379) | private void loadUrlAndJavascript(String url, String javascript) {
    method autoSelectItem (line 391) | public void autoSelectItem(String url) {
    method getHighlightDrawable (line 404) | private GradientDrawable getHighlightDrawable() {
    method getChildType (line 413) | @Override
    method getChildTypeCount (line 419) | @Override
    method getGroupType (line 424) | @Override
    method getGroupTypeCount (line 430) | @Override

FILE: app/src/main/java/io/gonative/android/LoginManager.java
  class LoginManager (line 21) | public class LoginManager extends Observable {
    method LoginManager (line 29) | LoginManager(Context context) {
    method checkLogin (line 34) | public void checkLogin() {
    method isLoggedIn (line 47) | public boolean isLoggedIn() {
    class CheckRedirectionTask (line 52) | private static class CheckRedirectionTask extends AsyncTask<String, Vo...
      method CheckRedirectionTask (line 55) | public CheckRedirectionTask(LoginManager loginManager) {
      method doInBackground (line 59) | @Override
      method onPostExecute (line 100) | @Override

FILE: app/src/main/java/io/gonative/android/MainActivity.java
  class MainActivity (line 102) | public class MainActivity extends AppCompatActivity implements Observer,
    method run (line 170) | @Override
    method onCreate (line 207) | @Override
    method getActivityId (line 571) | public String getActivityId() {
    method initialRootSetup (line 575) | private void initialRootSetup() {
    method setupProfilePicker (line 592) | private void setupProfilePicker() {
    method showNavigationMenu (line 600) | private void showNavigationMenu(boolean showNavigation) {
    method getUrlFromIntent (line 647) | private String getUrlFromIntent(Intent intent) {
    method onPause (line 673) | protected void onPause() {
    method onStart (line 696) | @Override
    method onResume (line 706) | @Override
    method onStop (line 734) | @Override
    method onDestroy (line 746) | @Override
    method onSubscriptionChanged (line 779) | @Override
    method launchNotificationActivity (line 785) | @Override
    method retryFailedPage (line 796) | private void retryFailedPage() {
    method onSaveInstanceState (line 814) | protected void onSaveInstanceState (Bundle outState) {
    method addToHistory (line 834) | public void addToHistory(String url) {
    method hearShake (line 849) | @Override
    method onClearCache (line 860) | @Override
    method canGoBack (line 866) | public boolean canGoBack() {
    method goBack (line 871) | public void goBack() {
    method canGoForward (line 882) | private boolean canGoForward() {
    method goForward (line 886) | private void goForward() {
    method sharePage (line 896) | @Override
    method logout (line 927) | private void logout() {
    method loadUrl (line 938) | public void loadUrl(String url) {
    method loadUrl (line 942) | public void loadUrl(String url, boolean isFromTab) {
    method loadUrlAndJavascript (line 956) | public void loadUrlAndJavascript(String url, String javascript) {
    method loadUrlAndJavascript (line 960) | public void loadUrlAndJavascript(String url, String javascript, boolea...
    method runJavascript (line 977) | public void runJavascript(String javascript) {
    method isDisconnected (line 982) | public boolean isDisconnected(){
    method clearWebviewCache (line 987) | @Override
    method clearWebviewCookies (line 992) | @Override
    method hideWebview (line 999) | @Override
    method showWebview (line 1019) | private void showWebview(double delay) {
    method showWebviewImmediately (line 1033) | public void showWebviewImmediately() {
    method showWebview (line 1045) | @Override
    method injectCSSviaJavascript (line 1070) | private void injectCSSviaJavascript() {
    method injectJSviaJavascript (line 1096) | private void injectJSviaJavascript() {
    method updatePageTitle (line 1122) | public void updatePageTitle() {
    method update (line 1128) | public void update (Observable sender, Object data) {
    method updateMenu (line 1134) | @Override
    method updateMenu (line 1139) | private void updateMenu(boolean isLoggedIn){
    method isDrawerOpen (line 1153) | private boolean isDrawerOpen() {
    method setDrawerEnabled (line 1157) | private void setDrawerEnabled(boolean enabled) {
    method setupMenu (line 1181) | private void setupMenu(){
    method onPostCreate (line 1195) | @Override
    method onConfigurationChanged (line 1206) | @Override
    method onActivityResult (line 1220) | @Override
    method cancelFileUpload (line 1338) | public void cancelFileUpload() {
    method onNewIntent (line 1352) | @Override
    method urlEqualsIgnoreSlash (line 1366) | private boolean urlEqualsIgnoreSlash(String url1, String url2) {
    method onKeyDown (line 1380) | @Override
    method switchToWebview (line 1419) | public void switchToWebview(GoNativeWebviewInterface newWebview, boole...
    method onCreateOptionsMenu (line 1467) | @Override
    method getOptionsMenu (line 1480) | public Menu getOptionsMenu () {
    method setMenuItemsVisible (line 1484) | public void setMenuItemsVisible (boolean visible) {
    method setMenuItemsVisible (line 1488) | public void setMenuItemsVisible(boolean visible, MenuItem exception) {
    method onOptionsItemSelected (line 1501) | @Override
    method onRefresh (line 1532) | @Override
    method stopNavAnimation (line 1538) | private void stopNavAnimation(boolean isConsumed){
    method stopNavAnimation (line 1542) | private void stopNavAnimation(boolean isConsumed, int delay){
    method refreshPage (line 1557) | public void refreshPage() {
    method checkNavigationForPage (line 1574) | @Override
    method checkPreNavigationForPage (line 1597) | @Override
    method getActionManager (line 1620) | public ActionManager getActionManager() {
    method setupTitleDisplayForUrl (line 1624) | @Override
    method urlLevelForUrl (line 1630) | @Override
    method titleForUrl (line 1646) | @Override
    method closeDrawers (line 1666) | public void closeDrawers() {
    method isNotRoot (line 1670) | public boolean isNotRoot() {
    method getParentUrlLevel (line 1674) | @Override
    method getUrlLevel (line 1679) | @Override
    method setUrlLevel (line 1684) | @Override
    method getProfilePicker (line 1689) | public ProfilePicker getProfilePicker() {
    method getFileDownloader (line 1693) | public FileDownloader getFileDownloader() {
    method getFileWriterSharer (line 1697) | public FileWriterSharer getFileWriterSharer() {
    method getStatusCheckerBridge (line 1701) | public StatusCheckerBridge getStatusCheckerBridge() {
    method setTitle (line 1705) | @Override
    method startCheckingReadyStatus (line 1713) | @Override
    method stopCheckingReadyStatus (line 1718) | private void stopCheckingReadyStatus() {
    method checkReadyStatus (line 1722) | private void checkReadyStatus() {
    method checkReadyStatusResult (line 1726) | private void checkReadyStatusResult(String status) {
    method showTabs (line 1745) | public void showTabs() {
    method hideTabs (line 1749) | public void hideTabs() {
    method toggleFullscreen (line 1753) | @Override
    method onRequestPermissionsResult (line 1794) | @Override
    method getGNWindowManager (line 1844) | public GoNativeWindowManager getGNWindowManager() {
    method getWindowCount (line 1848) | @Override
    method setUploadMessage (line 1853) | public void setUploadMessage(ValueCallback<Uri> mUploadMessage) {
    method setUploadMessageLP (line 1857) | public void setUploadMessageLP(ValueCallback<Uri[]> uploadMessageLP) {
    method setDirectUploadImageUri (line 1861) | public void setDirectUploadImageUri(Uri directUploadImageUri) {
    method getFullScreenLayout (line 1865) | public RelativeLayout getFullScreenLayout() {
    method getWebView (line 1869) | @Override
    class StatusCheckerBridge (line 1874) | public class StatusCheckerBridge {
      method onReadyState (line 1875) | @JavascriptInterface
    class ConnectivityChangeReceiver (line 1886) | private class ConnectivityChangeReceiver extends BroadcastReceiver {
      method onReceive (line 1887) | @Override
    method getRuntimeGeolocationPermission (line 1896) | public void getRuntimeGeolocationPermission(final GeolocationPermissio...
    method getPermission (line 1912) | public void getPermission(String[] permissions, PermissionCallback cal...
    method startActivityAfterPermissions (line 1939) | public void startActivityAfterPermissions(Intent intent) {
    method setScreenOrientationPreference (line 1947) | private void setScreenOrientationPreference() {
    method setDeviceOrientation (line 1971) | @SuppressLint("SourceLockedOrientationActivity")
    method getTabManager (line 1986) | public TabManager getTabManager() {
    type PermissionCallback (line 1990) | public interface PermissionCallback {
      method onPermissionResult (line 1991) | void onPermissionResult(String[] permissions, int[] grantResults);
    class PermissionsCallbackPair (line 1994) | private class PermissionsCallbackPair {
      method PermissionsCallbackPair (line 1998) | PermissionsCallbackPair(String[] permissions, PermissionCallback cal...
    method enableSwipeRefresh (line 2004) | public void enableSwipeRefresh() {
    method restoreSwipRefreshDefault (line 2010) | public void restoreSwipRefreshDefault() {
    method deselectTabs (line 2017) | @Override
    method listenForSignalStrength (line 2022) | private void listenForSignalStrength() {
    method sendConnectivityOnce (line 2049) | @Override
    method sendConnectivityOnce (line 2067) | private void sendConnectivityOnce() {
    method sendConnectivity (line 2073) | private void sendConnectivity(String callback) {
    method subscribeConnectivity (line 2111) | @Override
    method unsubscribeConnectivity (line 2123) | @Override
    type GeolocationPermissionCallback (line 2128) | public interface GeolocationPermissionCallback {
      method onResult (line 2129) | void onResult(boolean granted);
    method setBrightness (line 2133) | @Override
    method setSidebarNavigationEnabled (line 2140) | @Override
    method getDrawerLayout (line 2146) | public GoNativeDrawerLayout getDrawerLayout() {
    method getDrawerToggle (line 2150) | public ActionBarDrawerToggle getDrawerToggle() {
    method setupAppTheme (line 2158) | @Override
    method setupWebviewTheme (line 2176) | @SuppressLint("RequiresFeature")
    method validateGoogleService (line 2212) | private void validateGoogleService() {
    method isAndroidGestureEnabled (line 2223) | @SuppressLint("DiscouragedApi")
    method updateStatusBarOverlay (line 2241) | @Override
    method updateStatusBarStyle (line 2255) | @Override
    method setStatusBarColor (line 2287) | @Override
    method runGonativeDeviceInfo (line 2292) | @Override
    method windowFlag (line 2313) | @Override
    method setCustomTitle (line 2322) | @Override
    method downloadFile (line 2331) | @Override
    method selectTab (line 2336) | @Override
    method setTabsWithJson (line 2342) | @Override
    method focusAudio (line 2348) | @Override
    method clipboardSet (line 2357) | @Override
    method clipboardGet (line 2365) | @Override
    method sendRegistration (line 2386) | @Override
    method runCustomNativeBridge (line 2406) | @Override
    method promptLocationService (line 2420) | @Override
    method isLocationServiceEnabled (line 2425) | @Override
    method isLocationPermissionGranted (line 2442) | private boolean isLocationPermissionGranted() {
    method setRestoreBrightnessOnNavigation (line 2448) | @Override
    method isRestoreBrightnessOnNavigation (line 2453) | public boolean isRestoreBrightnessOnNavigation() {
    method getJavascriptBridge (line 2459) | public Object getJavascriptBridge() {
    method closeCurrentWindow (line 2464) | @Override
    method openNewWindow (line 2471) | @Override
    method onMaxWindowsReached (line 2487) | @Override
    method getKeyboardInfo (line 2552) | @Override
    method addKeyboardListener (line 2558) | @Override

FILE: app/src/main/java/io/gonative/android/MySwipeRefreshLayout.java
  class MySwipeRefreshLayout (line 12) | public class MySwipeRefreshLayout extends GoNativeSwipeRefreshLayout {
    type CanChildScrollUpCallback (line 15) | public interface CanChildScrollUpCallback {
      method canSwipeRefreshChildScrollUp (line 16) | boolean canSwipeRefreshChildScrollUp();
    method MySwipeRefreshLayout (line 19) | public MySwipeRefreshLayout(Context context) {
    method MySwipeRefreshLayout (line 23) | public MySwipeRefreshLayout(Context context, AttributeSet attrs) {
    method setCanChildScrollUpCallback (line 27) | public void setCanChildScrollUpCallback(CanChildScrollUpCallback canCh...
    method canChildScrollUp (line 31) | @Override

FILE: app/src/main/java/io/gonative/android/ProfilePicker.java
  class ProfilePicker (line 23) | public class ProfilePicker implements AdapterView.OnItemSelectedListener {
    method ProfilePicker (line 36) | public ProfilePicker(MainActivity mainActivity, Spinner spinner) {
    method parseJson (line 46) | private void parseJson(String s){
    method getAdapter (line 82) | private ArrayAdapter<String> getAdapter(){
    method onItemSelected (line 106) | public void onItemSelected(AdapterView<?> parent, View view, int posit...
    method onNothingSelected (line 115) | public void onNothingSelected(AdapterView<?> parent) {
    method getProfileJsBridge (line 119) | public ProfileJsBridge getProfileJsBridge() {
    class ProfileJsBridge (line 123) | public class ProfileJsBridge {
      method parseJson (line 124) | @JavascriptInterface

FILE: app/src/main/java/io/gonative/android/RegistrationManager.java
  class RegistrationManager (line 26) | public class RegistrationManager {
    method RegistrationManager (line 35) | RegistrationManager(Context context) {
    method processConfig (line 40) | public void processConfig(JSONArray endpoints) {
    method checkUrl (line 62) | public void checkUrl(String url) {
    method setCustomData (line 71) | public void setCustomData(JSONObject customData) {
    method sendToAllEndpoints (line 76) | public void sendToAllEndpoints() {
    method registrationDataChanged (line 82) | private void registrationDataChanged() {
    method subscriptionInfoChanged (line 91) | public void subscriptionInfoChanged(){
    class RegistrationEndpoint (line 95) | private class RegistrationEndpoint {
      method RegistrationEndpoint (line 99) | RegistrationEndpoint(String postUrl, List<Pattern> urlRegexes) {
      method sendRegistrationInfo (line 104) | void sendRegistrationInfo() {
    class SendRegistrationTask (line 109) | private static class SendRegistrationTask extends AsyncTask<Void,Void,...
      method SendRegistrationTask (line 114) | SendRegistrationTask(Context context, RegistrationEndpoint registrat...
      method doInBackground (line 120) | @Override

FILE: app/src/main/java/io/gonative/android/SegmentedController.java
  class SegmentedController (line 23) | public class SegmentedController implements AdapterView.OnItemSelectedLi...
    method SegmentedController (line 32) | SegmentedController(MainActivity mainActivity, Spinner spinner) {
    method updateSegmentedControl (line 58) | private void updateSegmentedControl() {
    method getAdapter (line 97) | private ArrayAdapter<String> getAdapter() {
    method onItemSelected (line 109) | @Override
    method onNothingSelected (line 124) | @Override

FILE: app/src/main/java/io/gonative/android/ShakeDialogFragment.java
  class ShakeDialogFragment (line 12) | public class ShakeDialogFragment extends DialogFragment {
    type ShakeDialogListener (line 13) | public interface ShakeDialogListener {
      method onClearCache (line 14) | public void onClearCache(DialogFragment dialog);
    method onCreateDialog (line 19) | @NonNull
    method onAttach (line 35) | @Override

FILE: app/src/main/java/io/gonative/android/SplashActivity.java
  class SplashActivity (line 24) | public class SplashActivity extends AppCompatActivity {
    method onCreate (line 27) | @Override
    method onRequestPermissionsResult (line 58) | @Override
    method handleSplash (line 64) | private void handleSplash(AppConfig config){
    method startMainActivity (line 84) | private void startMainActivity() {

FILE: app/src/main/java/io/gonative/android/TabManager.java
  class TabManager (line 32) | public class TabManager implements AHBottomNavigation.OnTabSelectedListe...
    method TabManager (line 49) | @SuppressWarnings("unused")
    method TabManager (line 54) | TabManager(MainActivity mainActivity, AHBottomNavigation bottomNavigat...
    method initializeTabMenus (line 86) | private void initializeTabMenus(){
    method checkTabs (line 109) | public void checkTabs(String url) {
    method setMenuID (line 145) | private void setMenuID(String id){
    method setTabs (line 162) | private void setTabs(JSONArray tabs) {
    method showTabs (line 205) | private void showTabs() {
    method hideTabs (line 214) | private void hideTabs() {
    method getRegexForTab (line 224) | private List<Pattern> getRegexForTab(JSONObject tabConfig) {
    method getCachedRegexForTab (line 233) | private List<Pattern> getCachedRegexForTab(int position) {
    method autoSelectTab (line 248) | public void autoSelectTab(String url) {
    method selectTab (line 264) | @SuppressWarnings("UnusedReturnValue")
    method setTabsWithJson (line 294) | public void setTabsWithJson(JSONObject tabsJson, int tabMenuId) {
    method selectTabNumber (line 318) | public void selectTabNumber(int tabNumber, boolean performAction) {
    method onTabSelected (line 326) | @Override
    class TabMenu (line 347) | private class TabMenu {

FILE: app/src/main/java/io/gonative/android/UrlInspector.java
  class UrlInspector (line 15) | public class UrlInspector {
    method getInstance (line 25) | public static UrlInspector getInstance(){
    method init (line 32) | public void init(Context context) {
    method UrlInspector (line 43) | private UrlInspector() {
    method inspectUrl (line 47) | public void inspectUrl(String url) {
    method getUserId (line 56) | public String getUserId() {
    method setUserId (line 60) | private void setUserId(String userId) {

FILE: app/src/main/java/io/gonative/android/UrlNavigation.java
  type WebviewLoadState (line 54) | enum WebviewLoadState {
  class UrlNavigation (line 61) | public class UrlNavigation {
    method UrlNavigation (line 88) | UrlNavigation(MainActivity activity) {
    method isInternalUri (line 108) | private boolean isInternalUri(Uri uri) {
    method shouldOverrideUrlLoading (line 136) | public boolean shouldOverrideUrlLoading(GoNativeWebviewInterface view,...
    method shouldOverrideUrlLoadingNoIntercept (line 141) | private boolean shouldOverrideUrlLoadingNoIntercept(final GoNativeWebv...
    method shouldOverrideUrlLoading (line 388) | public boolean shouldOverrideUrlLoading(final GoNativeWebviewInterface...
    method onPageStarted (line 430) | public void onPageStarted(String url) {
    method showWebViewImmediately (line 467) | @SuppressWarnings("unused")
    method onPageFinished (line 477) | @SuppressLint("ApplySharedPref")
    method injectJSBridgeLibrary (line 562) | private void injectJSBridgeLibrary(String currentWebviewUrl) {
    method onFormResubmission (line 581) | public void onFormResubmission(GoNativeWebviewInterface view, Message ...
    method runGonativeDeviceInfo (line 585) | private void runGonativeDeviceInfo(String callback) {
    method doUpdateVisitedHistory (line 600) | public void doUpdateVisitedHistory(@SuppressWarnings("unused") GoNativ...
    method onReceivedError (line 611) | public void onReceivedError(final GoNativeWebviewInterface view,
    method onReceivedSslError (line 666) | public void onReceivedSslError(SslError error, String webviewUrl) {
    method getCurrentWebviewUrl (line 689) | @SuppressWarnings("unused")
    method setCurrentWebviewUrl (line 694) | public void setCurrentWebviewUrl(String currentWebviewUrl) {
    method interceptHtml (line 699) | public WebResourceResponse interceptHtml(LeanWebView view, String url) {
    method chooseFileUpload (line 704) | @SuppressWarnings("UnusedReturnValue")
    method chooseFileUpload (line 709) | public boolean chooseFileUpload(final String[] mimetypespec, final boo...
    method chooseFileUploadAfterPermission (line 724) | @SuppressWarnings("UnusedReturnValue")
    method launchChooserIntent (line 738) | private boolean launchChooserIntent(FileUploadIntentsCreator creator) {
    method openDirectCamera (line 753) | public boolean openDirectCamera(final String[] mimetypespec, final boo...
    method openDirectCameraAfterPermission (line 778) | @SuppressWarnings("UnusedReturnValue")
    method createNewWindow (line 805) | @SuppressLint("SetJavaScriptEnabled")
    method createNewWindow (line 831) | private void createNewWindow(Message resultMsg, boolean maxWindowsEnab...
    method isLocationServiceEnabled (line 845) | public boolean isLocationServiceEnabled() {
    method onDownloadStart (line 857) | protected void onDownloadStart() {
    class GetKeyTask (line 863) | private static class GetKeyTask extends AsyncTask<String, Void, Pair<P...
      method GetKeyTask (line 867) | public GetKeyTask(Activity activity, ClientCertRequest request) {
      method doInBackground (line 872) | @Override
      method onPostExecute (line 886) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method onReceivedClientCertRequest (line 897) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method cancelLoadTimeout (line 914) | public void cancelLoadTimeout() {

FILE: app/src/main/java/io/gonative/android/WebViewPool.java
  class WebViewPool (line 34) | public class WebViewPool {
    class WebViewPoolCallback (line 35) | public class WebViewPoolCallback {
      method onPageFinished (line 36) | @SuppressWarnings("unused")
      method interceptHtml (line 49) | public WebResourceResponse interceptHtml(GoNativeWebviewInterface we...
    method init (line 71) | public void init(Activity activity) {
    method processConfig (line 128) | private void processConfig() {
    method resumeLoading (line 181) | private void resumeLoading() {
    method flushAll (line 228) | private void flushAll() {
    method disownWebview (line 237) | public void disownWebview(GoNativeWebviewInterface webview) {
    method webviewForUrl (line 248) | public Pair<GoNativeWebviewInterface, WebViewPoolDisownPolicy> webview...
    method urlSetForUrl (line 268) | private HashSet<String> urlSetForUrl(String url){

FILE: app/src/main/java/io/gonative/android/WebViewPoolDisownPolicy.java
  type WebViewPoolDisownPolicy (line 6) | public enum WebViewPoolDisownPolicy {

FILE: app/src/main/java/io/gonative/android/widget/CircleImageView.java
  class CircleImageView (line 22) | class CircleImageView extends androidx.appcompat.widget.AppCompatImageVi...
    method CircleImageView (line 35) | CircleImageView(Context context, int color) {
    method elevationSupported (line 61) | private boolean elevationSupported() {
    method onMeasure (line 65) | @Override
    method setAnimationListener (line 74) | public void setAnimationListener(Animation.AnimationListener listener) {
    method onAnimationStart (line 78) | @Override
    method onAnimationEnd (line 86) | @Override
    method setBackgroundColorRes (line 99) | public void setBackgroundColorRes(int colorRes) {
    method setBackgroundColor (line 103) | @Override
    class OvalShadow (line 110) | private class OvalShadow extends OvalShape {
      method OvalShadow (line 114) | OvalShadow(int shadowRadius) {
      method onResize (line 121) | @Override
      method draw (line 127) | @Override
      method updateRadialGradient (line 135) | private void updateRadialGradient(int diameter) {

FILE: app/src/main/java/io/gonative/android/widget/GoNativeDrawerLayout.java
  class GoNativeDrawerLayout (line 12) | public class GoNativeDrawerLayout extends DrawerLayout {
    method GoNativeDrawerLayout (line 16) | public GoNativeDrawerLayout(@NonNull Context context) {
    method GoNativeDrawerLayout (line 20) | public GoNativeDrawerLayout(@NonNull Context context, @Nullable Attrib...
    method GoNativeDrawerLayout (line 24) | public GoNativeDrawerLayout(@NonNull Context context, @Nullable Attrib...
    method onInterceptTouchEvent (line 28) | @Override
    method onTouchEvent (line 38) | @Override
    method setDisableTouch (line 47) | public void setDisableTouch(boolean disableTouch){

FILE: app/src/main/java/io/gonative/android/widget/GoNativeSwipeRefreshLayout.java
  class GoNativeSwipeRefreshLayout (line 55) | public class GoNativeSwipeRefreshLayout extends ViewGroup implements Nes...
    method onAnimationStart (line 165) | @Override
    method onAnimationRepeat (line 169) | @Override
    method onAnimationEnd (line 173) | @Override
    method reset (line 191) | void reset() {
    method setEnabled (line 205) | @Override
    method onDetachedFromWindow (line 213) | @Override
    method setColorViewAlpha (line 219) | private void setColorViewAlpha(int targetAlpha) {
    method setProgressViewOffset (line 243) | public void setProgressViewOffset(boolean scale, int start, int end) {
    method getProgressViewStartOffset (line 256) | public int getProgressViewStartOffset() {
    method getProgressViewEndOffset (line 264) | public int getProgressViewEndOffset() {
    method setProgressViewEndTarget (line 281) | public void setProgressViewEndTarget(boolean scale, int end) {
    method setSlingshotDistance (line 295) | public void setSlingshotDistance(@Px int slingshotDistance) {
    method setSize (line 302) | public void setSize(int size) {
    method GoNativeSwipeRefreshLayout (line 325) | public GoNativeSwipeRefreshLayout(@NonNull Context context) {
    method GoNativeSwipeRefreshLayout (line 335) | public GoNativeSwipeRefreshLayout(@NonNull Context context, @Nullable ...
    method getChildDrawingOrder (line 367) | @Override
    method createProgressView (line 383) | private void createProgressView() {
    method setOnRefreshListener (line 396) | public void setOnRefreshListener(@Nullable GoNativeSwipeRefreshLayout....
    method setRefreshing (line 406) | public void setRefreshing(boolean refreshing) {
    method startScaleUpAnimation (line 424) | private void startScaleUpAnimation(Animation.AnimationListener listene...
    method setAnimationProgress (line 445) | void setAnimationProgress(float progress) {
    method setRefreshing (line 450) | private void setRefreshing(boolean refreshing, final boolean notify) {
    method startScaleDownAnimation (line 463) | void startScaleDownAnimation(Animation.AnimationListener listener) {
    method startProgressAlphaStartAnimation (line 476) | private void startProgressAlphaStartAnimation() {
    method startProgressAlphaMaxAnimation (line 480) | private void startProgressAlphaMaxAnimation() {
    method startAlphaAnimation (line 484) | private Animation startAlphaAnimation(final int startingAlpha, final i...
    method setProgressBackgroundColor (line 503) | @Deprecated
    method setProgressBackgroundColorSchemeResource (line 513) | public void setProgressBackgroundColorSchemeResource(@ColorRes int col...
    method setProgressBackgroundColorSchemeColor (line 522) | public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {
    method setColorScheme (line 529) | @Deprecated
    method setColorSchemeResources (line 541) | public void setColorSchemeResources(@ColorRes int... colorResIds) {
    method setColorSchemeColors (line 557) | public void setColorSchemeColors(@ColorInt int... colors) {
    method isRefreshing (line 566) | public boolean isRefreshing() {
    method ensureTarget (line 570) | private void ensureTarget() {
    method setDistanceToTriggerSync (line 589) | public void setDistanceToTriggerSync(int distance) {
    method onLayout (line 593) | @Override
    method onMeasure (line 618) | @Override
    method getProgressCircleDiameter (line 649) | public int getProgressCircleDiameter() {
    method canChildScrollUp (line 657) | public boolean canChildScrollUp() {
    method setOnChildScrollUpCallback (line 672) | public void setOnChildScrollUpCallback(@Nullable GoNativeSwipeRefreshL...
    method onInterceptTouchEvent (line 676) | @Override
    method requestDisallowInterceptTouchEvent (line 747) | @Override
    method onStartNestedScroll (line 762) | @Override
    method onNestedScrollAccepted (line 768) | @Override
    method onNestedPreScroll (line 778) | @Override
    method getNestedScrollAxes (line 810) | @Override
    method onStopNestedScroll (line 815) | @Override
    method onNestedScroll (line 829) | @Override
    method setNestedScrollingEnabled (line 850) | @Override
    method isNestedScrollingEnabled (line 855) | @Override
    method startNestedScroll (line 860) | @Override
    method stopNestedScroll (line 865) | @Override
    method hasNestedScrollingParent (line 870) | @Override
    method dispatchNestedScroll (line 875) | @Override
    method dispatchNestedPreScroll (line 882) | @Override
    method onNestedPreFling (line 888) | @Override
    method onNestedFling (line 894) | @Override
    method dispatchNestedFling (line 900) | @Override
    method dispatchNestedPreFling (line 905) | @Override
    method isAnimationRunning (line 910) | private boolean isAnimationRunning(Animation animation) {
    method moveSpinner (line 914) | private void moveSpinner(float overscrollTop) {
    method finishSpinner (line 966) | private void finishSpinner(float overscrollTop) {
    method onTouchEvent (line 999) | @Override
    method startDragging (line 1089) | private void startDragging(float y) {
    method animateOffsetToCorrectPosition (line 1098) | private void animateOffsetToCorrectPosition(int from, Animation.Animat...
    method animateOffsetToStartPosition (line 1110) | private void animateOffsetToStartPosition(int from, Animation.Animatio...
    method applyTransformation (line 1128) | @Override
    method moveToStart (line 1144) | void moveToStart(float interpolatedTime) {
    method applyTransformation (line 1152) | @Override
    method startScaleDownReturnToStartAnimation (line 1158) | private void startScaleDownReturnToStartAnimation(int from,
    method setTargetOffsetTopAndBottom (line 1178) | void setTargetOffsetTopAndBottom(int offset) {
    method onSecondaryPointerUp (line 1184) | private void onSecondaryPointerUp(MotionEvent ev) {
    type OnRefreshListener (line 1199) | public interface OnRefreshListener {
      method onRefresh (line 1203) | void onRefresh();
    type OnChildScrollUpCallback (line 1210) | public interface OnChildScrollUpCallback {
      method canChildScrollUp (line 1220) | boolean canChildScrollUp(@NonNull GoNativeSwipeRefreshLayout parent,...

FILE: app/src/main/java/io/gonative/android/widget/WebViewContainerView.java
  class WebViewContainerView (line 24) | public class WebViewContainerView extends FrameLayout {
    method WebViewContainerView (line 29) | public WebViewContainerView(@NonNull Context context) {
    method WebViewContainerView (line 33) | public WebViewContainerView(@NonNull Context context, @Nullable Attrib...
    method WebViewContainerView (line 38) | public WebViewContainerView(@NonNull Context context, @Nullable Attrib...
    method initializeWebview (line 43) | private void initializeWebview(Context context) {
    method setupWebview (line 63) | public void setupWebview(MainActivity activity, boolean isRoot) {
    method getWebview (line 77) | public GoNativeWebviewInterface getWebview() {

FILE: app/src/normal/java/io/gonative/android/GoNativeWebChromeClient.java
  class GoNativeWebChromeClient (line 32) | class GoNativeWebChromeClient extends WebChromeClient {
    method GoNativeWebChromeClient (line 41) | public GoNativeWebChromeClient(MainActivity mainActivity, UrlNavigatio...
    method onJsAlert (line 51) | @Override
    method onJsBeforeUnload (line 60) | @Override
    method onGeolocationPermissionsShowPrompt (line 66) | @Override
    method onShowCustomView (line 98) | @Override
    method onHideCustomView (line 114) | @Override
    method exitFullScreen (line 132) | public boolean exitFullScreen() {
    method onCloseWindow (line 141) | @Override
    method onShowFileChooser (line 146) | @Override
    method openFileChooser (line 177) | public void openFileChooser(ValueCallback<Uri> uploadMsg, String accep...
    method openFileChooser (line 187) | public void openFileChooser(ValueCallback<Uri> uploadMsg, String accep...
    method openFileChooser (line 192) | public void openFileChooser(ValueCallback<Uri> uploadMsg) {
    method onReceivedTitle (line 196) | @Override
    method onCreateWindow (line 201) | @Override
    method onPermissionRequest (line 207) | @Override
    method onPermissionRequestCanceled (line 252) | @Override
    method onConsoleMessage (line 257) | @Override

FILE: app/src/normal/java/io/gonative/android/GoNativeWebviewClient.java
  class GoNativeWebviewClient (line 25) | public class GoNativeWebviewClient extends WebViewClient{
    method GoNativeWebviewClient (line 30) | public GoNativeWebviewClient(MainActivity mainActivity, UrlNavigation ...
    method shouldOverrideUrlLoading (line 35) | @Override
    method shouldOverrideUrlLoading (line 40) | public boolean shouldOverrideUrlLoading(WebView view, String url, bool...
    method shouldOverrideUrlLoading (line 44) | @Override
    method onPageStarted (line 53) | @Override
    method onPageFinished (line 60) | @Override
    method onFormResubmission (line 67) | @Override
    method doUpdateVisitedHistory (line 72) | @Override
    method onReceivedError (line 77) | @Override
    method onReceivedError (line 82) | @TargetApi(Build.VERSION_CODES.M)
    method onReceivedSslError (line 89) | @Override
    method onReceivedClientCertRequest (line 95) | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    method shouldInterceptRequest (line 101) | @Override
    method shouldInterceptRequest (line 106) | @TargetApi(Build.VERSION_CODES.LOLLIPOP)

FILE: app/src/normal/java/io/gonative/android/LeanWebView.java
  class LeanWebView (line 28) | public class LeanWebView extends WebView implements GoNativeWebviewInter...
    method LeanWebView (line 36) | public LeanWebView(Context context) {
    method LeanWebView (line 41) | public LeanWebView(Context context, AttributeSet attrs) {
    method LeanWebView (line 46) | public LeanWebView(Context context, AttributeSet attrs, int defStyle) {
    method onFling (line 52) | @Override
    method onScroll (line 58) | @Override
    method getScreenX (line 64) | private int getScreenX(){
    method onDown (line 70) | @Override
    method compareEvents (line 75) | private boolean compareEvents(MotionEvent event1, MotionEvent event2, ...
    method onTouchEvent (line 99) | @Override
    method setWebViewClient (line 105) | @Override
    method setWebChromeClient (line 111) | @Override
    method loadUrl (line 117) | @Override
    method reload (line 131) | @Override
    method goBack (line 138) | @Override
    method canGoForward (line 169) | @Override
    method goForward (line 184) | @Override
    method urlEqualsIgnoreSlash (line 202) | private boolean urlEqualsIgnoreSlash(String url1, String url2) {
    method loadUrlDirect (line 214) | public void loadUrlDirect(String url) {
    method checkLoginSignup (line 218) | public boolean checkLoginSignup() {
    method setCheckLoginSignup (line 222) | public void setCheckLoginSignup(boolean checkLoginSignup) {
    method runJavascript (line 226) | public void runJavascript(String js) {
    method exitFullScreen (line 259) | public boolean exitFullScreen() {
    method isCrosswalk (line 267) | static public boolean isCrosswalk() {
    method saveStateToBundle (line 271) | @Override
    method restoreStateFromBundle (line 276) | @Override
    method getWebViewScrollY (line 281) | @Override
    method getWebViewScrollX (line 286) | @Override
    method scrollTo (line 291) | @Override
    type OnSwipeListener (line 296) | public interface OnSwipeListener {
      method onSwipeLeft (line 297) | void onSwipeLeft();
      method onSwipeRight (line 298) | void onSwipeRight();
    method getOnSwipeListener (line 304) | public OnSwipeListener getOnSwipeListener() {
    method setOnSwipeListener (line 311) | public void setOnSwipeListener(OnSwipeListener onSwipeListener) {
    method getMaxHorizontalScroll (line 315) | @Override
    method zoomBy (line 320) | @Override
    method isZoomed (line 326) | @Override
    method zoomOut (line 331) | @Override

FILE: app/src/normal/java/io/gonative/android/PoolWebViewClient.java
  class PoolWebViewClient (line 16) | public class PoolWebViewClient extends WebViewClient {
    method PoolWebViewClient (line 19) | public PoolWebViewClient(WebViewPool.WebViewPoolCallback webViewPoolCa...
    method onPageFinished (line 24) | @Override
    method shouldInterceptRequest (line 39) | @Override
    method shouldInterceptRequest (line 44) | @TargetApi(Build.VERSION_CODES.LOLLIPOP)

FILE: app/src/normal/java/io/gonative/android/WebViewSetup.java
  class WebViewSetup (line 20) | public class WebViewSetup {
    method setupWebviewForActivity (line 23) | @SuppressLint("JavascriptInterface")
    method setupWebview (line 76) | @SuppressWarnings("deprecation")
    method setupWebviewGlobals (line 129) | public static void setupWebviewGlobals(Context context) {
    method removeCallbacks (line 140) | public static void removeCallbacks(LeanWebView webview) {

FILE: app/src/normal/java/io/gonative/android/WebkitCookieManagerProxy.java
  class WebkitCookieManagerProxy (line 18) | public class WebkitCookieManagerProxy extends java.net.CookieManager {
    method WebkitCookieManagerProxy (line 22) | public WebkitCookieManagerProxy()
    method WebkitCookieManagerProxy (line 27) | WebkitCookieManagerProxy(CookieStore store, CookiePolicy cookiePolicy)
    method put (line 35) | @Override
    method get (line 98) | @Override
    method getCookieStore (line 118) | @Override

FILE: generate-theme.js
  function showHelp (line 223) | function showHelp() {
  function checkColorRegex (line 234) | function checkColorRegex(color, message) {
  function createColorJSON (line 242) | function createColorJSON(colorName, colorCode, defaultColor) {
Condensed preview — 122 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (596K chars).
[
  {
    "path": ".github/workflows/firebase-testing.yml",
    "chars": 4167,
    "preview": "name: firebase-testing\non: [push, workflow_dispatch]\njobs:\n  test-app:\n    runs-on: ubuntu-latest\n    steps:\n      - use"
  },
  {
    "path": ".gitignore",
    "chars": 89,
    "preview": "*~\n.idea/\n.gradle/\n*.iml\nlocal.properties\napp/build/\nbuild/\ncaptures/\n.DS_Store\nplugins/\n"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 1019,
    "preview": "# Changelog\n\n## 2014-01-04\n\n- Fix a crash on reload with no page loaded.\n\n## 2015-01-02\n\n- Update to latest gradle and b"
  },
  {
    "path": "README.md",
    "chars": 486,
    "preview": "# Archive Notice\n\nThis repository is archived and is no longer being maintained. You can now build an app using our onli"
  },
  {
    "path": "app/.gitignore",
    "chars": 7,
    "preview": "build/\n"
  },
  {
    "path": "app/build.gradle",
    "chars": 6556,
    "preview": "import groovy.json.JsonSlurper\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n//[enabled by bui"
  },
  {
    "path": "app/proguard-project.txt",
    "chars": 1914,
    "preview": "# To enable ProGuard in your project, edit project.properties\n# to define the proguard.config property as described in t"
  },
  {
    "path": "app/src/androidTest/AndroidManifest.xml",
    "chars": 195,
    "preview": "<!-- Ignores minSdk 18 requirement error during build -->\n<manifest xmlns:tools=\"http://schemas.android.com/tools\">\n    "
  },
  {
    "path": "app/src/androidTest/assets/HelperClass.java",
    "chars": 103,
    "preview": "package io.gonative.android;\n\npublic class HelperClass {\n    public volatile static int newLoad = 0;\n}\n"
  },
  {
    "path": "app/src/androidTest/assets/appConfig.json",
    "chars": 4233,
    "preview": "{\n  \"general\": {\n    \"userAgentAdd\": \"gonative\",\n    \"initialUrl\": \"https://gonative.io\",\n    \"appName\": \"GoNative.io\"\n "
  },
  {
    "path": "app/src/androidTest/java/com/gonative/testFiles/FirstTestClass.java",
    "chars": 3524,
    "preview": "package com.gonative.testFiles;\n\nimport android.webkit.WebView;\nimport androidx.test.ext.junit.rules.ActivityScenarioRul"
  },
  {
    "path": "app/src/androidTest/java/com/gonative/testFiles/TestMethods.java",
    "chars": 8452,
    "preview": "package com.gonative.testFiles;\nimport android.webkit.WebView;\nimport androidx.test.espresso.NoMatchingViewException;\nim"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "chars": 5605,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:to"
  },
  {
    "path": "app/src/main/assets/BlobDownloader.js",
    "chars": 2182,
    "preview": "// This is used because download from native side won't have session changes.\n\nfunction gonativeDownloadBlobUrl(url) {\n\t"
  },
  {
    "path": "app/src/main/assets/GoNativeJSBridgeLibrary.js",
    "chars": 9867,
    "preview": "// this function accepts a callback function as params.callback that will be called with the command results\n// if a cal"
  },
  {
    "path": "app/src/main/assets/appConfig.json",
    "chars": 7040,
    "preview": "{\n  \"general\": {\n    \"appName\": \"GoNative.io\",\n    \"initialUrl\": \"https://gonative.io/\",\n    \"userAgentRegexes\": [],\n   "
  },
  {
    "path": "app/src/main/assets/custom-icons.json",
    "chars": 29,
    "preview": "{\n  \"gonative-icon\": 59392\n}\n"
  },
  {
    "path": "app/src/main/assets/offline.html",
    "chars": 3327,
    "preview": "<html>\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <title>Devi"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ActionManager.java",
    "chars": 24543,
    "preview": "package io.gonative.android;\n\nimport android.graphics.Color;\nimport android.graphics.PorterDuff;\nimport android.graphics"
  },
  {
    "path": "app/src/main/java/io/gonative/android/AppLinksActivity.java",
    "chars": 693,
    "preview": "package io.gonative.android;\n\nimport android.content.Intent;\nimport android.os.Bundle;\n\nimport androidx.annotation.Nulla"
  },
  {
    "path": "app/src/main/java/io/gonative/android/AudioUtils.java",
    "chars": 8065,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.media.AudioAttributes;\nimport android.media"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ConfigPreferences.java",
    "chars": 1208,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.p"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ConfigUpdater.java",
    "chars": 2397,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\n\nimport org.json.JSONExceptio"
  },
  {
    "path": "app/src/main/java/io/gonative/android/CustomHeaders.java",
    "chars": 2164,
    "preview": "package io.gonative.android;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os."
  },
  {
    "path": "app/src/main/java/io/gonative/android/DownloadService.java",
    "chars": 13746,
    "preview": "package io.gonative.android;\n\nimport android.app.Service;\nimport android.content.ActivityNotFoundException;\nimport andro"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileDownloader.java",
    "chars": 11342,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.ComponentName;\nimport android.content.Cont"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileUploadIntentsCreator.kt",
    "chars": 7613,
    "preview": "package io.gonative.android\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.*"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileWriterSharer.java",
    "chars": 12539,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.ActivityNotFoundException;\nimport android."
  },
  {
    "path": "app/src/main/java/io/gonative/android/GoNativeApplication.java",
    "chars": 3252,
    "preview": "package io.gonative.android;\n\nimport android.os.Message;\nimport android.webkit.ValueCallback;\nimport android.widget.Toas"
  },
  {
    "path": "app/src/main/java/io/gonative/android/GoNativeWindowManager.java",
    "chars": 4605,
    "preview": "package io.gonative.android;\n\nimport android.text.TextUtils;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npub"
  },
  {
    "path": "app/src/main/java/io/gonative/android/HtmlIntercept.java",
    "chars": 12036,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.text.TextUtils;\nimport android.util.Log;\nim"
  },
  {
    "path": "app/src/main/java/io/gonative/android/IOUtils.java",
    "chars": 704,
    "preview": "package io.gonative.android;\n\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport "
  },
  {
    "path": "app/src/main/java/io/gonative/android/Installation.java",
    "chars": 5468,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android."
  },
  {
    "path": "app/src/main/java/io/gonative/android/JsCustomCodeExecutor.java",
    "chars": 2023,
    "preview": "package io.gonative.android;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.Map;\n\nimport "
  },
  {
    "path": "app/src/main/java/io/gonative/android/JsResultBridge.java",
    "chars": 102,
    "preview": "package io.gonative.android;\n\npublic class JsResultBridge {\n    public static String jsResult = \"\";\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/JsonMenuAdapter.java",
    "chars": 15498,
    "preview": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.c"
  },
  {
    "path": "app/src/main/java/io/gonative/android/KeyboardManager.kt",
    "chars": 2482,
    "preview": "package io.gonative.android\n\nimport android.graphics.Rect\nimport android.text.TextUtils\nimport android.view.ViewGroup\nim"
  },
  {
    "path": "app/src/main/java/io/gonative/android/LoginManager.java",
    "chars": 4622,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\n\nimport org.json.JSONObject;\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/MainActivity.java",
    "chars": 95787,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.SuppressLint;\nimport android.content.Br"
  },
  {
    "path": "app/src/main/java/io/gonative/android/MySwipeRefreshLayout.java",
    "chars": 1092,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport io.gonative.andr"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ProfilePicker.java",
    "chars": 4321,
    "preview": "package io.gonative.android;\n\nimport androidx.annotation.NonNull;\nimport android.view.View;\nimport android.view.ViewGrou"
  },
  {
    "path": "app/src/main/java/io/gonative/android/RegistrationManager.java",
    "chars": 5600,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\nimport android.util.Log;\n\nimp"
  },
  {
    "path": "app/src/main/java/io/gonative/android/SegmentedController.java",
    "chars": 3966,
    "preview": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.c"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ShakeDialogFragment.java",
    "chars": 1491,
    "preview": "package io.gonative.android;\n\nimport android.app.AlertDialog;\nimport android.app.Dialog;\nimport android.content.Context;"
  },
  {
    "path": "app/src/main/java/io/gonative/android/SplashActivity.java",
    "chars": 3353,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.Intent;\nimport android.content.pm.PackageM"
  },
  {
    "path": "app/src/main/java/io/gonative/android/TabManager.java",
    "chars": 11663,
    "preview": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.c"
  },
  {
    "path": "app/src/main/java/io/gonative/android/UrlInspector.java",
    "chars": 1596,
    "preview": "package io.gonative.android;\n\nimport android.content.Context;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pa"
  },
  {
    "path": "app/src/main/java/io/gonative/android/UrlNavigation.java",
    "chars": 37067,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.SuppressLint;\nimport android.app.Activi"
  },
  {
    "path": "app/src/main/java/io/gonative/android/WebViewPool.java",
    "chars": 10598,
    "preview": "package io.gonative.android;\n\nimport android.app.Activity;\nimport android.content.BroadcastReceiver;\nimport android.cont"
  },
  {
    "path": "app/src/main/java/io/gonative/android/WebViewPoolDisownPolicy.java",
    "chars": 228,
    "preview": "package io.gonative.android;\n\n/**\n * Created by weiyin on 9/3/14.\n */\npublic enum WebViewPoolDisownPolicy {\n    Always, "
  },
  {
    "path": "app/src/main/java/io/gonative/android/files/CapturedImageSaver.kt",
    "chars": 1818,
    "preview": "package io.gonative.android.files\n\nimport android.content.ContentResolver\nimport android.content.ContentValues\nimport an"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/CircleImageView.java",
    "chars": 5103,
    "preview": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.grap"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/GoNativeDrawerLayout.java",
    "chars": 1391,
    "preview": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.ut"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/GoNativeSwipeRefreshLayout.java",
    "chars": 46660,
    "preview": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport andro"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/HandleView.kt",
    "chars": 5069,
    "preview": "package io.gonative.android.widget\n\nimport android.animation.ArgbEvaluator\nimport android.animation.ValueAnimator\nimport"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/SwipeHistoryNavigationLayout.kt",
    "chars": 15850,
    "preview": "package io.gonative.android.widget;\n\nimport android.animation.ObjectAnimator\nimport android.annotation.SuppressLint\nimpo"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/WebViewContainerView.java",
    "chars": 3107,
    "preview": "package io.gonative.android.widget;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.At"
  },
  {
    "path": "app/src/main/res/anim/fast_fade_out.xml",
    "chars": 223,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<alpha xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:fro"
  },
  {
    "path": "app/src/main/res/drawable/bg_nav_icon.xml",
    "chars": 272,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:in"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_back_24.xml",
    "chars": 388,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml",
    "chars": 383,
    "preview": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n  "
  },
  {
    "path": "app/src/main/res/drawable/ic_go_back.xml",
    "chars": 342,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#606060\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_go_forward.xml",
    "chars": 338,
    "preview": "<vector android:height=\"24dp\" android:tint=\"#606060\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    andr"
  },
  {
    "path": "app/src/main/res/drawable/ic_stat_onesignal_default.xml",
    "chars": 260,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- OneSignal requires an icon named ic_stat_onesignal_default. -->\n<!-- This al"
  },
  {
    "path": "app/src/main/res/drawable/shape_rounded.xml",
    "chars": 456,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item"
  },
  {
    "path": "app/src/main/res/layout/actionbar_title.xml",
    "chars": 798,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    "
  },
  {
    "path": "app/src/main/res/layout/activity_gonative.xml",
    "chars": 8032,
    "preview": "<io.gonative.android.widget.GoNativeDrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:ap"
  },
  {
    "path": "app/src/main/res/layout/activity_subscriptions.xml",
    "chars": 803,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xm"
  },
  {
    "path": "app/src/main/res/layout/button_menu.xml",
    "chars": 521,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmln"
  },
  {
    "path": "app/src/main/res/layout/empty.xml",
    "chars": 189,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    andro"
  },
  {
    "path": "app/src/main/res/layout/menu_child_icon.xml",
    "chars": 1370,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    an"
  },
  {
    "path": "app/src/main/res/layout/menu_child_noicon.xml",
    "chars": 787,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    an"
  },
  {
    "path": "app/src/main/res/layout/menu_group_icon.xml",
    "chars": 1837,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xm"
  },
  {
    "path": "app/src/main/res/layout/menu_group_noicon.xml",
    "chars": 1197,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    an"
  },
  {
    "path": "app/src/main/res/layout/profile_picker_dropdown.xml",
    "chars": 410,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android"
  },
  {
    "path": "app/src/main/res/layout/splash_screen.xml",
    "chars": 710,
    "preview": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    a"
  },
  {
    "path": "app/src/main/res/layout/tab.xml",
    "chars": 312,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android"
  },
  {
    "path": "app/src/main/res/layout/view_handle.xml",
    "chars": 1299,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xm"
  },
  {
    "path": "app/src/main/res/menu/topmenu.xml",
    "chars": 133,
    "preview": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    xmlns:app=\"http://schemas.android.com/apk/res-auto"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 265,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 265,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <b"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "chars": 1527,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <declare-styleable name=\"PagerSlidingTabStrip\">\n        <attr na"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "chars": 989,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#ffffff</color>\n    <color name=\"color"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "chars": 891,
    "preview": "<resources>\n\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizonta"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "chars": 120,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/integers.xml",
    "chars": 392,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <integer name=\"round_corner_dips\">15</integer>\r\n    <integer na"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "chars": 2982,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n\t<string name=\"app_name\">GoNative</string>\n\t<string name=\"action_sea"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "chars": 3624,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"GoNat"
  },
  {
    "path": "app/src/main/res/values-ko/strings.xml",
    "chars": 728,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"shake_to_clear_cache\">기기를 흔들면 캐시를 삭제할 수 있음</string>"
  },
  {
    "path": "app/src/main/res/values-large/styles.xml",
    "chars": 303,
    "preview": "<resources>\n\n    <style name=\"LoginFormContainer\">\n        <item name=\"android:layout_width\">400dp</item>\n        <item "
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "chars": 989,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#212121</color>\n    <color name=\"color"
  },
  {
    "path": "app/src/main/res/values-night/styles.xml",
    "chars": 998,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"GN.Da"
  },
  {
    "path": "app/src/main/res/values-night-v29/styles.xml",
    "chars": 966,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n   "
  },
  {
    "path": "app/src/main/res/values-sw600dp/attr.xml",
    "chars": 101,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <bool name=\"isTablet\">true</bool>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-sw600dp/dimens.xml",
    "chars": 203,
    "preview": "<resources>\r\n\r\n    <!--\r\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         scr"
  },
  {
    "path": "app/src/main/res/values-sw720dp-land/dimens.xml",
    "chars": 277,
    "preview": "<resources>\r\n\r\n    <!--\r\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         scr"
  },
  {
    "path": "app/src/main/res/values-v21/styles.xml",
    "chars": 826,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.LightWithDarkActionBar\" parent=\"Theme.AppCompat."
  },
  {
    "path": "app/src/main/res/values-v29/styles.xml",
    "chars": 964,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n   "
  },
  {
    "path": "app/src/main/res/xml/filepaths.xml",
    "chars": 397,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <cache-pa"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "chars": 252,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\">\n    "
  },
  {
    "path": "app/src/normal/java/io/gonative/android/GoNativeWebChromeClient.java",
    "chars": 10351,
    "preview": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.TargetApi;\nimport android.content.pm.Pa"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/GoNativeWebviewClient.java",
    "chars": 4490,
    "preview": "package io.gonative.android;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphi"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/LeanWebView.java",
    "chars": 11342,
    "preview": "package io.gonative.android;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Build;\nimpo"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/PoolWebViewClient.java",
    "chars": 1813,
    "preview": "package io.gonative.android;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.os.Handler;\ni"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/WebViewSetup.java",
    "chars": 5723,
    "preview": "package io.gonative.android;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os."
  },
  {
    "path": "app/src/normal/java/io/gonative/android/WebkitCookieManagerProxy.java",
    "chars": 4790,
    "preview": "package io.gonative.android;\n\nimport java.io.IOException;\nimport java.net.CookiePolicy;\nimport java.net.CookieStore;\nimp"
  },
  {
    "path": "build.gradle",
    "chars": 1015,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    ex"
  },
  {
    "path": "generate-app-icons.sh",
    "chars": 8274,
    "preview": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\nsips -z 1024 1024 -s format png --out $BASEDIR/AppIconTemp.png $BASEDIR/AppIcon 2>&1\n\n#"
  },
  {
    "path": "generate-header-images.sh",
    "chars": 2212,
    "preview": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\n\nsips --resampleHeight 36 -s format png --out $BASEDIR/app/src/main/res/drawable-mdpi/i"
  },
  {
    "path": "generate-plugin-icons.sh",
    "chars": 3019,
    "preview": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\n\n# intercom notification icon\nif [[ $1 == \"intercom\" ]]\nthen\n  cp $BASEDIR/app/src/main"
  },
  {
    "path": "generate-theme.js",
    "chars": 10990,
    "preview": "#!/usr/bin/env node\n'use strict';\n\nvar fs = require('fs'), xml2js = require('xml2js');\nvar builder = new xml2js.Builder("
  },
  {
    "path": "generate-tinted-icons.sh",
    "chars": 1753,
    "preview": "#!/usr/bin/env bash\nset -e\n\nDARK_ICONS=(\nic_refresh_white_24dp.png\nic_search_white_24dp.png\nic_share_white_24dp.png\n)\n\nL"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "chars": 230,
    "preview": "#Sat Feb 18 14:21:02 EST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_"
  },
  {
    "path": "gradle.properties",
    "chars": 128,
    "preview": "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m\nandroid.enableJetifier=true\nandroid.useAndroidX=true\nenableLogsInRelea"
  },
  {
    "path": "gradlew",
    "chars": 5080,
    "preview": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start "
  },
  {
    "path": "gradlew.bat",
    "chars": 2404,
    "preview": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@r"
  },
  {
    "path": "plugins.gradle",
    "chars": 4889,
    "preview": "import groovy.json.JsonSlurper\nimport org.gradle.initialization.DefaultSettings\nimport org.apache.tools.ant.taskdefs.con"
  },
  {
    "path": "settings.gradle",
    "chars": 187,
    "preview": "rootProject.name = 'GoNative'\nSystem.setProperty(\"user.dir\", rootProject.projectDir.toString())\napply from: file(\"./plug"
  }
]

// ... and 4 more files (download for full content)

About this extraction

This page contains the full source code of the gonativeio/gonative-android GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 122 files (554.2 KB), approximately 123.6k tokens, and a symbol index with 685 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!