[
  {
    "path": ".github/workflows/firebase-testing.yml",
    "content": "name: firebase-testing\non: [push, workflow_dispatch]\njobs:\n  test-app:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v2\n      - name: Set up gcloud Cloud SDK environment\n        # You may pin to the exact commit or the version.\n        # uses: google-github-actions/setup-gcloud@94337306dda8180d967a56932ceb4ddcf01edae7\n        uses: google-github-actions/setup-gcloud@v0.2.0\n        with:\n          # 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\".\n          version: latest\n          # 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.\n\n          # 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.\n          service_account_key: ${{ secrets.GCLOUD_KEY }}\n          # 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.\n          project_id: gn-test-firebase-test-lab\n          # 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.\n          export_default_credentials: true\n\n      - name: Make gradlew executable\n        run: chmod +x ./gradlew\n\n      - name: Copying the appConfig file from androidTest to main\n        run: cp -f ./app/src/androidTest/assets/appConfig.json ./app/src/main/assets/appConfig.json\n        \n      - name: Add dependencies in app/build.gradle\n        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\n\n      - name: Add test runner in defaultConfig in app/build.gradle\n        run: sed -i \"s/defaultConfig *\\n*{/defaultConfig {\\ntestInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'/\" ./app/build.gradle\n      \n      - name: Adding Helper method in GoNativeWebviewClient.java\n        run: sed -i \"s/super.onPageFinished *( *view *, *url *) *;/super.onPageFinished(view, url);\\nHelperClass.newLoad++;/\" ./app/src/normal/java/io/gonative/android/GoNativeWebviewClient.java\n      \n      - name: Copying HelperClass.java from androidTest/assets to normal/java/io/gonative/android/\n        run: cp -f ./app/src/androidTest/assets/HelperClass.java ./app/src/normal/java/io/gonative/android/HelperClass.java\n\n      - name: Build the App\n        run: ./gradlew assembleDebug assembleAndroidTest\n    \n      - name: Testing the App\n        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\n"
  },
  {
    "path": ".gitignore",
    "content": "*~\n.idea/\n.gradle/\n*.iml\nlocal.properties\napp/build/\nbuild/\ncaptures/\n.DS_Store\nplugins/\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 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 build tools versions, making the project compatible with Android Studio 1.0.\n- Fix bugs related to syncing of tabs with sidebar menu.\n\n## 2014-12-23\n\n- Allow setting of viewport while preserving ability to zoom.\n- Allow dynamic config of navigation title image URLs.\n- Various bug fixes involving javascript after page load, and tab coloring, tab animations, and a crash on application resume.\n\n## 2014-12-22\n\n- Fix various threading bugs where UI methods were called from non-UI threads.\n\n## 2014-12-05\n\n- Support showing the navigation title image on specific URLs.\n\n## 2014-12-03\n\n- Support customizing user agent per URL.\n- Add color styling options for tabs.\n\n## 2014-11-30\n\n- New tabs with better material design and animations.\n- Fix some automatic icon generation scripts.\n\n## 2014-11-26\n\n- Fix a crash involving webview pools.\n\n## 2014-11-25\n\n- Add support for custom actions in action bar.\n"
  },
  {
    "path": "README.md",
    "content": "# Archive Notice\n\nThis 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.\n\n\ngonative-android\n================\n\nThis is the native Android code previously used by [GoNative](https://median.co).\n\nIt allows the creation of apps from existing mobile-optimized websites.\n\nHow to use\n------------\nImport into Android Studio. Edit appConfig.json as appropriate.\n"
  },
  {
    "path": "app/.gitignore",
    "content": "build/\n"
  },
  {
    "path": "app/build.gradle",
    "content": "import groovy.json.JsonSlurper\n\napply plugin: 'com.android.application'\napply plugin: 'kotlin-android'\n//[enabled by builder] apply plugin: 'com.google.gms.google-services'\n//[enabled by builder] apply plugin: 'com.google.firebase.crashlytics'\n\next {\n    fbAppId = \"\"\n    fbClientToken = \"\"\n    onesignalAppId = \"\"\n    adMobAppId = \"\"\n    googleServiceInvalid = \"false\"\n    auth0Domain = \"\"\n    auth0Scheme = \"\"\n}\n\ntask parseAppConfig {\n    def jsonFile = file('src/main/assets/appConfig.json')\n    def parsedJson = new JsonSlurper().parseText(jsonFile.text)\n    if (parsedJson.services.facebook) {\n        if (parsedJson.services.facebook.appId) {\n            fbAppId = parsedJson.services.facebook.appId\n        }\n        if (parsedJson.services.facebook.clientToken) {\n            fbClientToken = parsedJson.services.facebook.clientToken\n        }\n    }\n    if (parsedJson.services.socialLogin && parsedJson.services.socialLogin.facebookLogin) {\n        if (parsedJson.services.socialLogin.facebookLogin.appId) {\n            fbAppId = parsedJson.services.socialLogin.facebookLogin.appId\n        }\n        if (parsedJson.services.socialLogin.facebookLogin.clientToken) {\n            fbClientToken = parsedJson.services.socialLogin.facebookLogin.clientToken\n        }\n    }\n    if (parsedJson.services.oneSignal && parsedJson.services.oneSignal.applicationId) {\n        onesignalAppId = parsedJson.services.oneSignal.applicationId\n    }\n    if (parsedJson.services.admob && parsedJson.services.admob.admobAndroid && parsedJson.services.admob.admobAndroid.applicationId) {\n        adMobAppId = parsedJson.services.admob.admobAndroid.applicationId\n    }\n    if (parsedJson.services.braze) {\n        if (parsedJson.services.braze.androidApiKey) {\n            gradle.ext.set(\"braze_api_key\", parsedJson.services.braze.androidApiKey)\n        }\n        if (parsedJson.services.braze.androidEndpointKey) {\n            gradle.ext.set(\"braze_endpoint_key\", parsedJson.services.braze.androidEndpointKey)\n        }\n    }\n    if (parsedJson.services.auth0) {\n        if (parsedJson.services.auth0.domain) {\n            auth0Domain = parsedJson.services.auth0.domain\n        }\n        if (parsedJson.services.auth0.scheme) {\n            auth0Scheme = parsedJson.services.auth0.scheme\n        }\n    }\n}\n\ntask checkGoogleService {\n    plugins.withId(\"com.google.gms.google-services\") {\n        def googleServiceJsonFile = file('google-services.json')\n        if (project.file(googleServiceJsonFile).exists()) {\n            if (googleServiceJsonFile.text.isEmpty()) {\n                googleServiceInvalid = \"true\"\n            }\n        } else {\n            googleServiceInvalid = \"true\"\n        }\n    }\n}\n\nbuild.dependsOn parseAppConfig\nbuild.dependsOn checkGoogleService\n\nandroid {\n    defaultConfig {\n        compileSdk 33\n        minSdkVersion 21\n        targetSdkVersion 33\n        applicationId \"io.gonative.android\"\n        versionCode 1\n        multiDexEnabled true\n        vectorDrawables.useSupportLibrary = true\n\n        manifestPlaceholders = [manifestApplicationId: \"${applicationId}\",\n                                onesignal_app_id: onesignalAppId,\n                                onesignal_google_project_number: \"\",\n                                admob_app_id: adMobAppId,\n                                facebook_app_id: fbAppId,\n                                facebook_client_token: fbClientToken,\n                                auth0Domain: auth0Domain, auth0Scheme: auth0Scheme ]\n    }\n\n    compileOptions {\n        sourceCompatibility JavaVersion.VERSION_1_8\n        targetCompatibility JavaVersion.VERSION_1_8\n    }\n\n    signingConfigs {\n        release {\n            storeFile file(\"../../release.keystore\")\n            storePassword \"password\"\n            keyAlias \"release\"\n            keyPassword \"password\"\n        }\n        upload {\n            storeFile file(\"../../upload.keystore\")\n            storePassword \"password\"\n            keyAlias \"upload\"\n            keyPassword \"password\"\n        }\n    }\n\n    buildTypes {\n        debug {\n\t        applicationIdSuffix \".debug\"\n        }\n        release {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'\n            zipAlignEnabled true\n            debuggable project.getProperties().get(\"enableLogsInRelease\").toBoolean()\n            signingConfig signingConfigs.release\n        }\n        upload {\n            minifyEnabled true\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-project.txt'\n            zipAlignEnabled true\n            matchingFallbacks = ['release']\n            debuggable project.getProperties().get(\"enableLogsInRelease\").toBoolean()\n            signingConfig signingConfigs.upload\n        }\n        buildTypes.each {\n            it.buildConfigField 'boolean', 'GOOGLE_SERVICE_INVALID', googleServiceInvalid\n        }\n    }\n\n    flavorDimensions \"webview\"\n\n    productFlavors {\n        normal {\n            dimension \"webview\"\n        }\n    }\n    namespace 'io.gonative.android'\n    testNamespace '${applicationId}.test'\n}\n\ndependencies {\n    /**** dependencies used by all apps ****/\n    implementation \"androidx.core:core-ktx:1.10.1\"\n    implementation \"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version\"\n    implementation 'com.aurelhubert:ahbottomnavigation:2.3.4'\n    implementation 'com.squareup:seismic:1.0.2'\n    implementation 'androidx.webkit:webkit:1.7.0'\n    implementation 'androidx.core:core-splashscreen:1.0.1'\n    implementation \"com.github.gonativeio:gonative-icons:$iconsVersion\"\n    implementation \"com.github.gonativeio:gonative-android-core:$coreVersion\"\n    /**** end all apps ****/\n\n    /**** add-on module dependencies ****/\n    /**** end modules ****/\n    \n    /**** Google Android and Play Services dependencies ****/\n    implementation 'androidx.multidex:multidex:2.0.1'\n    implementation 'androidx.cardview:cardview:1.0.0'\n    implementation 'androidx.browser:browser:1.5.0'\n    implementation 'androidx.appcompat:appcompat:1.6.1'\n    implementation 'com.google.android.material:material:1.9.0'\n    implementation \"androidx.drawerlayout:drawerlayout:1.2.0\"\n    implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'\n    /**** end google ****/\n\n    /**** local dependencies ****/\n    implementation fileTree(dir: 'libs', include: '*.jar')\n    implementation fileTree(dir: 'libs', include: '*.aar')\n    /**** end local ****/\n}\n\napply from: file(\"../plugins.gradle\"); applyNativeModulesAppBuildGradle(project)"
  },
  {
    "path": "app/proguard-project.txt",
    "content": "# To enable ProGuard in your project, edit project.properties\n# to define the proguard.config property as described in that file.\n#\n# Add project specific ProGuard rules here.\n# By default, the flags in this file are appended to flags specified\n# in ${sdk.dir}/tools/proguard/proguard-android.txt\n# You can edit the include path and order by changing the ProGuard\n# include property in project.properties.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# Add any project specific keep options here:\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# webview interfaces\n\n-keepclassmembers class io.gonative.android.ProfilePicker$ProfileJsBridge {\n    <methods>;\n}\n-keepclassmembers class io.gonative.android.MainActivity$StatusCheckerBridge {\n    <methods>;\n}\n-keepattributes JavascriptInterface\n\n# stuff for google play services\n-keep class * extends java.util.ListResourceBundle {\n    protected Object[][] getContents();\n}\n\n-keep public class com.google.android.gms.common.internal.safeparcel.SafeParcelable {\n    public static final *** NULL;\n}\n\n-keepnames @com.google.android.gms.common.annotation.KeepName class *\n-keepclassmembernames class * {\n    @com.google.android.gms.common.annotation.KeepName *;\n}\n\n-keepnames class * implements android.os.Parcelable {\n    public static final ** CREATOR;\n}\n\n# Google Cloud Messaging\n-keep class com.google.android.gms.** { *; }\n-keep interface com.google.android.gms.** { *; }\n\n# appcompat library\n-dontwarn android.support.v4.**\n-keep class android.support.v4.** { *; }\n-keep interface android.support.v4.** { *; }\n-dontwarn android.support.v7.**\n-keep class android.support.v7.** { *; }\n-keep interface android.support.v7.** { *; }\n"
  },
  {
    "path": "app/src/androidTest/AndroidManifest.xml",
    "content": "<!-- Ignores minSdk 18 requirement error during build -->\n<manifest xmlns:tools=\"http://schemas.android.com/tools\">\n    <uses-sdk tools:overrideLibrary=\"android_libs.ub_uiautomator\"/>\n</manifest>"
  },
  {
    "path": "app/src/androidTest/assets/HelperClass.java",
    "content": "package io.gonative.android;\n\npublic class HelperClass {\n    public volatile static int newLoad = 0;\n}\n"
  },
  {
    "path": "app/src/androidTest/assets/appConfig.json",
    "content": "{\n  \"general\": {\n    \"userAgentAdd\": \"gonative\",\n    \"initialUrl\": \"https://gonative.io\",\n    \"appName\": \"GoNative.io\"\n  },\n  \"navigation\": {\n    \"androidPullToRefresh\": true,\n    \"sidebarNavigation\": {\n      \"sidebarEnabledRegex\": null,\n      \"menus\": [{\n        \"name\": \"default\",\n        \"items\": [{\n          \"url\": \"https://gonative.io\",\n          \"label\": \"Home\",\n          \"subLinks\": []\n        }, {\n          \"url\": \"https://gonative.io/about\",\n          \"label\": \"About\",\n          \"subLinks\": []\n        }, {\n          \"url\": \"https://gonative.io/examples\",\n          \"label\": \"Examples\",\n          \"subLinks\": []\n        }],\n        \"active\": true\n      }]\n    },\n    \"tabNavigation\": {\n      \"tabSelectionConfig\": [{\n        \"id\": \"1\",\n        \"regex\": \".*about.*\"\n      }],\n      \"tabMenus\": [{\n        \"id\": \"1\",\n        \"items\": [{\n          \"icon\": \"fa-cloud\",\n          \"label\": \"Tab 1\",\n          \"url\": \"https://www.gonative.io/pricing\"\n        }, {\n          \"icon\": \"fa-globe\",\n          \"label\": \"Tab 2\",\n          \"url\": \"https://www.gonative.io/examples\"\n        }, {\n          \"icon\": \"fa-users\",\n          \"label\": \"Tab 3\",\n          \"url\": \"javascript:alert('You selected tab 3. These tabs are only shown on the about page')\"\n        }]\n      }],\n      \"active\": true\n    },\n    \"actionConfig\": {\n      \"active\": true,\n      \"actions\": [{\n        \"id\": \"exampleActions\",\n        \"items\": [{\n          \"label\": \"Globe\",\n          \"icon\": \"fa-globe\",\n          \"url\": \"javascript:alert('You tapped the globe! It only appears on the Examples page')\"\n        }]\n      }],\n      \"actionSelection\": [{\n        \"regex\": \".*/examples.*\",\n        \"id\": \"exampleActions\"\n      }]\n    },\n    \"regexInternalExternal\": {\n      \"rules\": [{\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/login.php.*\",\n        \"internal\": true\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/pages/.*\",\n        \"internal\": false\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/sharer\\\\.php.*\",\n        \"internal\": false\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*plus\\\\.google\\\\.com/share.*\",\n        \"internal\": false\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*twitter\\\\.com/intent/.*\",\n        \"internal\": false\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*gonative\\\\.io/?.*\",\n        \"internal\": true\n      }, {\n        \"regex\": \"https?://([-\\\\w]+\\\\.)*google\\\\.com/?.*\",\n        \"internal\": true\n      }, {\n        \"regex\": \"https://gonative-test-web.web.app/.*\",\n        \"internal\": true\n      },\n        {\n          \"regex\": \"https://us-central1-gn-test-firebase-test-lab.cloudfunctions.net/.*\",\n          \"internal\": true\n        }],\n      \"active\": true\n    },\n    \"redirects\": [{\n      \"from\": \"https://example.com/from/\",\n      \"to\": \"https://example.com/to/\"\n    }]\n  },\n  \"forms\": {\n    \"search\": {\n      \"active\": true,\n      \"searchTemplateURL\": \"https://us-central1-gn-test-firebase-test-lab.cloudfunctions.net/gnTestSearch?q=\"\n    }\n  },\n  \"styling\": {\n    \"showActionBar\": true,\n    \"showNavigationBar\": true,\n    \"iosTitleColor\": \"#333333\",\n    \"iosTintColor\": \"#0091fe\",\n    \"androidTheme\": \"Light.DarkActionBar\",\n    \"androidSidebarBackgroundColor\": \"#111111\",\n    \"androidSidebarForegroundColor\": \"#d0d0d0\",\n    \"androidHideTitleInActionBar\": false,\n    \"androidPullToRefreshColor\": \"#333333\",\n    \"androidTabBarBackgroundColor\": \"#fefefe\",\n    \"androidTabBarTextColor\": \"#747474\",\n    \"androidTabBarIndicatorColorx\": \"#2f79fe\",\n    \"androidShowSplash\": true,\n    \"androidShowSplashMaxTime\": null,\n    \"androidShowSplashForceTime\": null,\n    \"disableAnimations\": false,\n    \"menuAnimationDuration\": 0.15,\n    \"transitionInteractiveDelayMax\": 0.2\n  },\n  \"permissions\": {\n    \"usesGeolocation\": false,\n    \"androidDownloadToPublicStorage\": false\n  },\n  \"services\": {\n    \"oneSignal\": {\n      \"active\": false,\n      \"applicationId\": \"\"\n    },\n    \"facebook\": {\n      \"active\": false,\n      \"appId\": \"\",\n      \"displayName\": \"\"\n    },\n    \"registration\": {\n      \"active\": false,\n      \"endpoints\": [{\n        \"url\": \"https://gonative.io/example_push_endpoint\",\n        \"dataType\": \"onesignal\",\n        \"urlRegex\": \".*/loginfinished\"\n      }]\n    }\n  }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/gonative/testFiles/FirstTestClass.java",
    "content": "package com.gonative.testFiles;\n\nimport android.webkit.WebView;\nimport androidx.test.ext.junit.rules.ActivityScenarioRule;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.filters.LargeTest;\nimport androidx.test.filters.SdkSuppress;\nimport androidx.test.uiautomator.UiDevice;\nimport org.json.JSONException;\nimport org.junit.Before;\nimport org.junit.Rule;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\nimport io.gonative.android.MainActivity;\nimport io.gonative.android.R;\nimport io.gonative.gonative_core.AppConfig;\nimport static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;\n\n@RunWith(AndroidJUnit4.class)\n@SdkSuppress(minSdkVersion = 18)\n@LargeTest\npublic class FirstTestClass{\n    TestMethods testMethods;\n    AppConfig appConfig;\n    WebView webView;\n    private UiDevice uiDevice;\n\n    @Rule\n    public ActivityScenarioRule<MainActivity> activityScenarioRule = new ActivityScenarioRule<>(MainActivity.class);\n\n    @Before\n    public void initMethod() throws InterruptedException {\n        for(int i = 0; i < 10; i++){\n            try{\n                uiDevice = UiDevice.getInstance(getInstrumentation());\n            }catch (RuntimeException runtimeException){\n                Thread.sleep(2000);\n                continue;\n            }\n            Thread.sleep(1000);\n            break;\n        }\n        activityScenarioRule.getScenario().onActivity(activity -> {\n            appConfig = AppConfig.getInstance(activity);\n            webView = activity.findViewById(R.id.webview);\n            testMethods = new TestMethods(activity, webView);\n        });\n    }\n\n    //Sidebar Navigation Test\n    @Test\n    public void testSidebarNavigation() throws InterruptedException, JSONException {\n        if(appConfig.showNavigationMenu && (appConfig.menus.get(\"default\") != null)){\n            if(appConfig.menus.get(\"default\") == null) throw new RuntimeException(\"Navigation drawer list not found.\");\n            else {\n                testMethods.waitForPageLoaded();\n                testMethods.testNavigation(appConfig.menus.get(\"default\"));\n            }\n        }\n    }\n\n    //Tab Menu Navigation Test\n    @Test\n    public void testTabMenuNavigation() throws JSONException, InterruptedException {\n        if(appConfig.tabMenuRegexes.size() == 0) throw new RuntimeException(\"No Tab Menus found.\");\n        else{\n            testMethods.waitForPageLoaded();\n            testMethods.m_testTabNavigation(appConfig.tabMenus, appConfig.tabMenuRegexes);\n        }\n    }\n\n    //Internal vs External Links Test\n    @Test\n    public void testIvE() throws InterruptedException {\n        testMethods.waitForPageLoaded();\n        testMethods.testInternalvExternalLinks(uiDevice);\n    }\n\n    //Pull to Refresh Test\n    @Test\n    public void pullToRefresh() throws InterruptedException {\n        if(appConfig.pullToRefresh){\n            testMethods.waitForPageLoaded();\n            testMethods.testPullToRefresh();\n        }\n    }\n\n    //Search Button Test\n    @Test\n    public void testSearch() throws InterruptedException {\n        if(appConfig.searchTemplateUrl != null && !appConfig.searchTemplateUrl.isEmpty()){\n            testMethods.waitForPageLoaded();\n            testMethods.testSearchButton();\n        }\n    }\n\n    //Refresh Button Test\n    @Test\n    public void testRefreshButton() throws InterruptedException {\n        if(appConfig.showRefreshButton){\n            testMethods.waitForPageLoaded();\n            testMethods.testRefreshButton();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/androidTest/java/com/gonative/testFiles/TestMethods.java",
    "content": "package com.gonative.testFiles;\nimport android.webkit.WebView;\nimport androidx.test.espresso.NoMatchingViewException;\nimport androidx.test.espresso.PerformException;\nimport androidx.test.espresso.contrib.DrawerActions;\nimport androidx.test.espresso.web.webdriver.Locator;\nimport androidx.test.uiautomator.UiDevice;\nimport io.gonative.android.HelperClass;\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.regex.Pattern;\nimport static androidx.test.espresso.Espresso.onData;\nimport static androidx.test.espresso.Espresso.onView;\nimport static androidx.test.espresso.Espresso.pressBackUnconditionally;\nimport static androidx.test.espresso.action.ViewActions.click;\nimport static androidx.test.espresso.action.ViewActions.pressImeActionButton;\nimport static androidx.test.espresso.action.ViewActions.swipeDown;\nimport static androidx.test.espresso.action.ViewActions.typeText;\nimport static androidx.test.espresso.matcher.ViewMatchers.withId;\nimport static androidx.test.espresso.matcher.ViewMatchers.withResourceName;\nimport static androidx.test.espresso.matcher.ViewMatchers.withText;\nimport static androidx.test.espresso.web.assertion.WebViewAssertions.webMatches;\nimport static androidx.test.espresso.web.sugar.Web.onWebView;\nimport static androidx.test.espresso.web.webdriver.DriverAtoms.findElement;\nimport static androidx.test.espresso.web.webdriver.DriverAtoms.getText;\nimport static androidx.test.espresso.web.webdriver.DriverAtoms.webClick;\nimport static org.hamcrest.Matchers.anything;\nimport static org.hamcrest.Matchers.equalTo;\nimport io.gonative.android.MainActivity;\nimport io.gonative.android.R;\n\npublic class TestMethods {\n\n    public TestMethods(MainActivity mainActivity, WebView webView){\n        m_mainActivity = mainActivity;\n        m_webView = webView;\n    }\n\n    protected static String currentURL = \"URL Not assigned yet\";\n    protected static String prevURL = \"URL Not assigned yet\";\n    private final WebView m_webView;\n    private final MainActivity m_mainActivity;\n\n    private void m_UpdateCurrentURL() throws InterruptedException {\n        m_mainActivity.runOnUiThread(() -> currentURL = m_webView.getOriginalUrl());\n        Thread.sleep(1000);\n    }\n\n    protected int getNewLoad(){\n        return HelperClass.newLoad;\n    }\n\n    public boolean isURL(String url){\n        try {\n            new URL(url);\n            return true;\n        }catch (MalformedURLException e){\n            return false;\n        }\n    }\n\n    public void waitForPageLoaded() throws InterruptedException {\n        int counter = 0;\n        while (getNewLoad() == 0) {\n            if (counter >= 15) throw new RuntimeException(\"Page failed to load in less than 15 seconds.\");\n            Thread.sleep(1000);\n            counter++;\n        }\n        m_UpdateCurrentURL();\n        Thread.sleep(1000);\n        counter = 0;\n        while(currentURL == null && counter <= 10){\n            Thread.sleep(1000);\n            counter++;\n            m_UpdateCurrentURL();\n        }\n        if(currentURL != null) HelperClass.newLoad = 0;\n        else throw new RuntimeException(\"Current URL cannot be retrieved from the WebView.\");\n    }\n\n    public void testNavigation(JSONArray sidebarObjects) throws InterruptedException, JSONException {\n        for(int i = 0; i < sidebarObjects.length(); i++){\n            onView(withId(R.id.drawer_layout)).perform(DrawerActions.open());\n            onData(anything()).inAdapterView(withId(R.id.drawer_list)).atPosition(i).perform(click());\n            waitForPageLoaded();\n            String sidebarURL = sidebarObjects.getJSONObject(i).getString(\"url\");\n            if(!currentURL.matches(sidebarURL)) throw new RuntimeException(\"Sidebar Menu \" + (i+1) + \" did not load the designated URL - \" + sidebarURL);\n        }\n    }\n\n    public void testInternalvExternalLinks(UiDevice uiDevice) throws InterruptedException {\n        m_mainActivity.runOnUiThread(() -> m_webView.loadUrl(\"https://gonative-test-web.web.app/\"));\n        waitForPageLoaded();\n        onWebView().withElement(findElement(Locator.ID, \"facebook_link\")).perform(webClick());\n        waitForPageLoaded();\n        String dFacebook = uiDevice.getCurrentPackageName();\n\n        if(dFacebook.contains(\"io.gonative.android\")){\n            uiDevice.pressBack();\n            Thread.sleep(2000);\n        }else throw new RuntimeException(\"Facebook link opened externally\");\n\n        onWebView().withElement(findElement(Locator.ID, \"twitter_link\")).perform(webClick());\n        Thread.sleep(8000);\n        String dTwitter = uiDevice.getCurrentPackageName();\n        if(dTwitter.contains(\"io.gonative.android\")) throw new RuntimeException(\"Twitter opened internally.\");\n        else {\n            uiDevice.pressBack();\n            Thread.sleep(1000);\n        }\n    }\n\n    public void testRefreshButton() throws InterruptedException {\n        onView(withId(R.id.action_refresh)).perform(click());\n        waitForPageLoaded();\n    }\n\n    public void testSearchButton() throws InterruptedException {\n        String query = \"Gonative\";\n        onView(withId(R.id.action_search)).perform(click());\n        Thread.sleep(1000);\n        onView(withResourceName(\"search_src_text\")).perform(typeText(query), pressImeActionButton());\n        waitForPageLoaded();\n        try{\n            onWebView().withElement(findElement(Locator.ID, \"search_param\")).check(webMatches(getText(), equalTo(query)));\n        }catch (Exception exception){\n            throw new RuntimeException(\"Search button failed to load the results with query - \" + query);\n        }\n        pressBackUnconditionally();\n        Thread.sleep(2000);\n    }\n\n    public void m_testTabNavigation(HashMap<String, JSONArray> tabMenus, ArrayList<Pattern> tabMenuRegexes) throws JSONException, InterruptedException {\n        while(!(currentURL.matches(tabMenuRegexes.get(0).pattern()))){\n            m_mainActivity.runOnUiThread(() -> m_webView.loadUrl(\"https://gonative.io/about/\"));\n            waitForPageLoaded();\n        }\n        if (tabMenus.size() == 0) throw new RuntimeException(\"No Tab Menus Added.\");\n        else {\n            for (Pattern p : tabMenuRegexes) {\n                if (currentURL.matches(p.pattern())) {\n                    try {\n                        for (String i : tabMenus.keySet()) {\n                            for (int j = 0; j < tabMenus.get(i).length(); ) {\n                                if (currentURL.matches(p.pattern())) {\n                                    if (isURL(tabMenus.get(i).getJSONObject(j).get(\"url\").toString())) {\n                                        Thread.sleep(1000);\n                                        onView(withText(tabMenus.get(i).getJSONObject(j).get(\"label\").toString())).perform(click());\n                                        waitForPageLoaded();\n                                        prevURL = currentURL;\n                                        pressBackUnconditionally();\n                                        waitForPageLoaded();\n                                        String tabURL = tabMenus.get(i).getJSONObject(j).get(\"url\").toString();\n                                        if (!(prevURL.matches(tabURL))) throw new RuntimeException(\"Tab \" + (j+1) + \" could not load the designated URL - \" + prevURL);\n                                        j++;\n                                        prevURL = currentURL;\n                                    } else {\n                                        onView(withText(tabMenus.get(i).getJSONObject(j).get(\"label\").toString())).perform(click());\n                                        j++;\n                                        Thread.sleep(2000);\n                                    }\n                                }\n                            }\n                        }\n                    } catch (NoMatchingViewException | PerformException noMatchingViewException) {\n                        throw new RuntimeException(\"Tab Menu not displayed in the desired regex: \" + p.pattern());\n                    }\n                } else{\n                    throw new RuntimeException(\"No Tab Menus found on the current page.\");\n                }\n            }\n        }\n    }\n\n    public void testPullToRefresh() throws InterruptedException {\n        onView(withId(R.id.webview)).perform(swipeDown());\n        waitForPageLoaded();\n    }\n}\n"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:versionName=\"1.0.0\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <!-- <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" /> -->\n    <!-- <uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\" /> -->\n    <uses-permission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n    <uses-permission android:name=\"android.permission.READ_PHONE_STATE\" />\n    <uses-permission\n        android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\"\n        android:maxSdkVersion=\"28\" />\n    <uses-permission android:name=\"android.permission.VIBRATE\" />\n    <uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n\n    <!-- Camera permissions -->\n    <!--<uses-permission android:name=\"android.permission.CAMERA\"/>-->\n    <!--<uses-feature android:name=\"android.hardware.camera\" android:required=\"false\" />-->\n\n    <!-- Microphone permissions -->\n    <!--<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>-->\n    <!--<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>-->\n\n    <!-- Bluetooth permissions -->\n    <!--<uses-permission android:name=\"android.permission.BLUETOOTH\" />-->\n    <!--<uses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\" />-->\n\n    <!-- permissions for push messages -->\n    <uses-permission android:name=\"com.google.android.c2dm.permission.RECEIVE\" />\n    <permission\n        android:name=\"${applicationId}.permission.C2D_MESSAGE\"\n        android:protectionLevel=\"signature\" />\n    <uses-permission android:name=\"${applicationId}.permission.C2D_MESSAGE\" />\n\n    <!-- permissions to block phone calls -->\n    <!--<uses-permission android:name=\"android.permission.READ_CONTACTS\" />-->\n    <!--<uses-permission android:name=\"android.permission.READ_CALL_LOG\" />-->\n    <!--<uses-permission android:name=\"android.permission.ANSWER_PHONE_CALLS\" />-->\n\n    <queries>\n        <!-- Camera -->\n        <intent>\n            <action android:name=\"android.media.action.IMAGE_CAPTURE\" />\n        </intent>\n        <intent>\n            <action android:name=\"android.media.action.VIDEO_CAPTURE\" />\n        </intent>\n\n        <!-- Gallery -->\n        <intent>\n            <action android:name=\"android.intent.action.GET_CONTENT\" />\n            <data android:mimeType=\"image/*\" />\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.PICK\" />\n            <data android:mimeType=\"image/*\" />\n        </intent>\n        <intent>\n            <action android:name=\"android.intent.action.CHOOSER\" />\n        </intent>\n    </queries>\n\n    <application\n        android:name=\".GoNativeApplication\"\n        android:allowBackup=\"true\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:logo=\"@drawable/ic_actionbar\"\n        android:networkSecurityConfig=\"@xml/network_security_config\"\n        android:requestLegacyExternalStorage=\"true\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/GoNativeTheme.NoActionBar\">\n\n        <activity\n            android:name=\"io.gonative.android.MainActivity\"\n            android:configChanges=\"orientation|screenSize\"\n            android:windowSoftInputMode=\"adjustResize\"\n            android:exported=\"true\"\n            android:label=\"@string/app_name\"\n            tools:node=\"merge\" />\n\n        <activity\n            android:name=\".AppLinksActivity\"\n            android:exported=\"true\"\n            android:launchMode=\"singleTask\">\n            <!--additional intent filters-->\n\n            <!--example: -->\n            <!--<intent-filter>-->\n            <!--<action android:name=\"android.intent.action.VIEW\"></action>-->\n            <!--<category android:name=\"android.intent.category.DEFAULT\"></category>-->\n            <!--<category android:name=\"android.intent.category.BROWSABLE\"></category>-->\n            <!--<data android:scheme=\"http\"></data>-->\n            <!--<data android:scheme=\"https\"></data>-->\n            <!--<data android:host=\"gonative.io\"></data>-->\n            <!--<data android:pathPrefix=\"/\"></data>-->\n            <!--</intent-filter>-->\n        </activity>\n\n        <!-- For file sharing without having to use external permissions. -->\n        <provider\n            android:name=\"androidx.core.content.FileProvider\"\n            android:authorities=\"${applicationId}.fileprovider\"\n            android:exported=\"false\"\n            android:grantUriPermissions=\"true\">\n            <meta-data\n                android:name=\"android.support.FILE_PROVIDER_PATHS\"\n                android:resource=\"@xml/filepaths\" />\n        </provider>\n\n        <!-- Facebook -->\n        <meta-data\n            android:name=\"com.facebook.sdk.ApplicationId\"\n            android:value=\"fb${facebook_app_id}\" />\n        <meta-data\n            android:name=\"com.facebook.sdk.ClientToken\"\n            android:value=\"${facebook_client_token}\" />\n\n        <activity\n            android:name=\".SplashActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/SplashTheme\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <service android:name=\".DownloadService\"/>\n\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/assets/BlobDownloader.js",
    "content": "// This is used because download from native side won't have session changes.\n\nfunction gonativeDownloadBlobUrl(url) {\n\tvar req = new XMLHttpRequest();\n\treq.open('GET', url, true);\n\treq.responseType = 'blob';\n\n\treq.onload = function(event) {\n\t\tvar blob = req.response;\n\t\tsaveBlob(blob);\n\t};\n\treq.send();\n\n\tfunction sendMessage(message) {\n\t    if (window.webkit && window.webkit.messageHandlers &&\n\t        window.webkit.messageHandlers.fileWriterSharer) {\n\t        window.webkit.messageHandlers.fileWriterSharer.postMessage(message);\n\t    }\n\t    if (window.gonative_file_writer_sharer && window.gonative_file_writer_sharer.postMessage) {\n\t\t\twindow.gonative_file_writer_sharer.postMessage(JSON.stringify(message));\n\t    }\n\t}\n\n\tfunction saveBlob(blob, filename) {\n\t    var chunkSize = 1024 * 1024; // 1mb\n\t    var index = 0;\n\t    // random string to identify this file transfer\n\t    var id = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);\n\n\t    function sendHeader() {\n\t        sendMessage({\n\t            event: 'fileStart',\n\t            id: id,\n\t            size: blob.size,\n\t            type: blob.type,\n\t            name: filename\n\t        });\n\t    }\n\n\t    function sendChunk() {\n\t        if (index >= blob.size) {\n\t            return sendEnd();\n\t        }\n\n\t        var chunkToSend = blob.slice(index, index + chunkSize);\n\t        var reader = new FileReader();\n\t        reader.readAsDataURL(chunkToSend);\n\t        reader.onloadend = function() {\n\t            sendMessage({\n\t                event: 'fileChunk',\n\t                id: id,\n\t                data: reader.result\n\t            });\n\t            index += chunkSize;\n\t            setTimeout(sendChunk);\n\t        };\n\t    }\n\t    \n\t    function sendEnd() {\n\t        sendMessage({\n\t            event: 'fileEnd',\n\t            id:id\n\t        });\n\t    }\n\t    \n\t    sendHeader();\n\t    gonative_run_after_storage_permissions.push(sendChunk);\n\t}\n}\n\ngonative_run_after_storage_permissions = [];\nfunction gonativeGotStoragePermissions() {\n    while (gonative_run_after_storage_permissions.length > 0) {\n        var run = gonative_run_after_storage_permissions.shift();\n        run();\n    }\n}\n"
  },
  {
    "path": "app/src/main/assets/GoNativeJSBridgeLibrary.js",
    "content": "// this function accepts a callback function as params.callback that will be called with the command results\n// if a callback is not provided it returns a promise that will resolve with the command results\nfunction addCommandCallback(command, params, persistCallback) {\n    if(params && (params.callback || params.callbackFunction)){\n        // execute command with provided callback function\n        addCommand(command, params, persistCallback);\n    } else {\n        // create a temporary function and return a promise that executes command\n        var tempFunctionName = '_gonative_temp_' + Math.random().toString(36).slice(2);\n        if(!params) params = {};\n        params.callback = tempFunctionName;\n        return new Promise(function(resolve, reject) {\n            // declare a temporary function\n            window[tempFunctionName] = function(data) {\n                resolve(data);\n                delete window[tempFunctionName];\n            }\n            // execute command\n            addCommand(command, params);\n        });\n    }\n}\n\nfunction addCallbackFunction(callbackFunction, persistCallback){\n    var callbackName;\n    if(typeof callbackFunction === 'string'){\n        callbackName = callbackFunction;\n    } else {\n        callbackName = '_gonative_temp_' + Math.random().toString(36).slice(2);\n        window[callbackName] = function(...args) {\n            callbackFunction.apply(null, args);\n            if(!persistCallback){ // if callback is used just once\n                delete window[callbackName];\n            }\n        }\n    }\n    return callbackName;\n}\n\nfunction addCommand(command, params, persistCallback){\n    var data = undefined;\n    if(params) {\n        var commandObject = {};\n        if(params.callback && typeof params.callback === 'function'){\n            params.callback = addCallbackFunction(params.callback, persistCallback);\n        }\n        if(params.callbackFunction && typeof params.callbackFunction === 'function'){\n            params.callbackFunction = addCallbackFunction(params.callbackFunction, persistCallback);\n        }\n        if(params.statuscallback && typeof params.statuscallback === 'function'){\n            params.statuscallback = addCallbackFunction(params.statuscallback, persistCallback);\n        }\n        commandObject.gonativeCommand = command;\n        commandObject.data = params;\n        data = JSON.stringify(commandObject);\n    } else data = command;\n\n    JSBridge.postMessage(data);\n}\n\n///////////////////////////////\n////    General Commands   ////\n///////////////////////////////\n\nvar gonative = {};\n\n// to be modified as required\ngonative.nativebridge = {\n    custom: function (params){\n        addCommand(\"gonative://nativebridge/custom\", params);\n    },\n    multi: function (params){\n        addCommand(\"gonative://nativebridge/multi\", params);\n    }\n};\n\ngonative.registration = {\n    send: function(customData){\n        var params = {customData: customData};\n        addCommand(\"gonative://registration/send\", params);\n    }\n};\n\ngonative.sidebar = {\n    setItems: function (params){\n        addCommand(\"gonative://sidebar/setItems\", params);\n    },\n    getItems: function (params){\n        return addCommandCallback(\"gonative://sidebar/getItems\", params);\n    }\n};\n\ngonative.tabNavigation = {\n    selectTab: function (tabIndex){\n        addCommand(\"gonative://tabs/select/\" + tabIndex);\n    },\n    deselect: function (){\n        addCommand(\"gonative://tabs/deselect\");\n    },\n    setTabs: function (tabsObject){\n        var params = {tabs: tabsObject};\n        addCommand(\"gonative://tabs/setTabs\", params);\n    }\n};\n\ngonative.share = {\n    sharePage: function (params){\n        addCommand(\"gonative://share/sharePage\", params);\n    },\n    downloadFile: function (params){\n        addCommand(\"gonative://share/downloadFile\", params);\n    },\n    downloadImage: function(params){\n        addCommand(\"gonative://share/downloadImage\", params);\n    }\n};\n\ngonative.open = {\n    appSettings: function (){\n        addCommand(\"gonative://open/app-settings\");\n    }\n};\n\ngonative.webview = {\n    clearCache: function(){\n        addCommand(\"gonative://webview/clearCache\");\n    },\n    clearCookies: function(){\n        addCommand(\"gonative://webview/clearCookies\");\n    },\n    reload: function (){\n        addCommand(\"gonative://webview/reload\");\n    }\n};\n\ngonative.config = {\n    set: function(params){\n        addCommand(\"gonative://config/set\", params);\n    }\n};\n\ngonative.navigationTitles = {\n    set: function (parameters){\n        var params = {\n            persist: parameters.persist,\n            data: parameters\n        };\n        addCommand(\"gonative://navigationTitles/set\", params);\n    },\n    setCurrent: function (params){\n        addCommand(\"gonative://navigationTitles/setCurrent\", params);\n    },\n    revert: function(){\n        addCommand(\"gonative://navigationTitles/set?persist=true\");\n    }\n};\n\ngonative.navigationLevels = {\n    set: function (parameters){\n        var params = {\n            persist: parameters.persist,\n            data: parameters\n        };\n        addCommand(\"gonative://navigationLevels/set\", params);\n    },\n    setCurrent: function(params){\n        addCommand(\"gonative://navigationLevels/set\", params);\n    },\n    revert: function(){\n        addCommand(\"gonative://navigationLevels/set?persist=true\");\n    }\n};\n\ngonative.statusbar = {\n    set: function (params){\n        addCommand(\"gonative://statusbar/set\", params);\n    }\n};\n\ngonative.screen = {\n    setBrightness: function(data){\n        var params = data;\n        if(typeof params === 'number'){\n            params = {brightness: data};\n        }\n        addCommand(\"gonative://screen/setBrightness\", params);\n    },\n    setMode: function(params) {\n        if (params.mode) {\n            addCommand(\"gonative://screen/setMode\", params);\n        }\n    }\n};\n\ngonative.navigationMaxWindows = {\n    set: function (maxWindows, autoClose){\n        var params = {\n            data: maxWindows,\n            autoClose: autoClose,\n            persist: true\n        };\n        addCommand(\"gonative://navigationMaxWindows/set\", params);\n    },\n    setCurrent: function(maxWindows, autoClose){\n        var params = {data: maxWindows, autoClose: autoClose};\n        addCommand(\"gonative://navigationMaxWindows/set\", params);\n    }\n}\n\ngonative.window = {\n    open: function (urlString) {\n        var params = {url: urlString};\n        addCommand(\"gonative://window/open\", params);\n    },\n    close: function () {\n        addCommand(\"gonative://window/close\");\n    }\n}\n\ngonative.connectivity = {\n    get: function (params){\n        return addCommandCallback(\"gonative://connectivity/get\", params);\n    },\n    subscribe: function (params){\n        return addCommandCallback(\"gonative://connectivity/subscribe\", params, true);\n    },\n    unsubscribe: function (){\n        addCommand(\"gonative://connectivity/unsubscribe\");\n    }\n};\n\ngonative.run = {\n    deviceInfo: function(){\n        addCommand(\"gonative://run/gonative_device_info\");\n    }\n};\n\ngonative.deviceInfo = function(params){\n    return addCommandCallback(\"gonative://run/gonative_device_info\", params, true);\n};\n\ngonative.internalExternal = {\n    set: function(params){\n        addCommand(\"gonative://internalExternal/set\", params);\n    }\n};\n\ngonative.clipboard = {\n    set: function(params){\n        addCommand(\"gonative://clipboard/set\", params);\n    },\n    get: function(params){\n        return addCommandCallback(\"gonative://clipboard/get\", params);\n    }\n};\n\ngonative.keyboard = {\n    info: function(params){\n        return addCommandCallback(\"gonative://keyboard/info\", params);\n    },\n    listen: function(callback){\n        var params = {callback};\n        addCommand(\"gonative://keyboard/listen\", params);\n    }\n};\n\n//////////////////////////////////////\n////   Webpage Helper Functions   ////\n//////////////////////////////////////\n\nfunction gonative_match_statusbar_to_body_background_color() {\n    let rgb = window.getComputedStyle(document.body, null).getPropertyValue('background-color');\n    let sep = rgb.indexOf(\",\") > -1 ? \",\" : \" \";\n    rgb = rgb.substring(rgb.indexOf('(')+1).split(\")\")[0].split(sep).map(function(x) { return x * 1; });\n    if(rgb.length === 4){\n        rgb = rgb.map(function(x){ return parseInt(x * rgb[3]); })\n    }\n    let hex = '#' + rgb[0].toString(16).padStart(2,'0') + rgb[1].toString(16).padStart(2,'0') + rgb[2].toString(16).padStart(2,'0');\n    let luma = 0.2126 * rgb[0] + 0.7152 * rgb[1] + 0.0722 * rgb[2]; // per ITU-R BT.709\n    if(luma > 40){\n        gonative.statusbar.set({'style': 'dark', 'color': hex});\n    }\n    else{\n        gonative.statusbar.set({'style': 'light', 'color': hex});\n    }\n}\n\n///////////////////////////////\n////   Android Exclusive   ////\n///////////////////////////////\n\ngonative.android = {};\n\ngonative.android.geoLocation = {\n    promptLocationServices: function(){\n        addCommand(\"gonative://geoLocation/promptLocationServices\");\n    },\n    isLocationServicesEnabled: function(params) {\n        return addCommandCallback(\"gonative://geoLocation/isLocationServicesEnabled\", params);\n    }\n};\n\ngonative.android.screen = {\n    fullscreen: function(){\n        addCommand(\"gonative://screen/fullscreen\");\n    },\n    normal: function(){\n        addCommand(\"gonative://screen/normal\");\n    },\n    keepScreenOn: function(){\n        addCommand(\"gonative://screen/keepScreenOn\");\n    },\n    keepScreenNormal: function(){\n        addCommand(\"gonative://screen/keepScreenNormal\");\n    }\n};\n\ngonative.android.audio = {\n    requestFocus: function(enabled){\n        var params = {enabled: enabled};\n        addCommand(\"gonative://audio/requestFocus\", params);\n    }\n};\n\ngonative.android.swipeGestures = {\n    enable: function() {\n        addCommand(\"gonative://swipeGestures/enable\");\n    },\n    disable: function() {\n        addCommand(\"gonative://swipeGestures/disable\");\n    }\n}\n"
  },
  {
    "path": "app/src/main/assets/appConfig.json",
    "content": "{\n  \"general\": {\n    \"appName\": \"GoNative.io\",\n    \"initialUrl\": \"https://gonative.io/\",\n    \"userAgentRegexes\": [],\n    \"replaceStrings\": [],\n    \"nativeBridgeUrls\": [],\n    \"languages\": [],\n    \"userAgentAdd\": \"gonative\",\n    \"enableWindowOpen\": true,\n    \"forceScreenOrientation\": null\n  },\n  \"navigation\": {\n    \"tabNavigation\": {\n      \"tabSelectionConfig\": [\n        {\n          \"id\": \"1\",\n          \"regex\": \"https://gonative.io/\"\n        }\n      ],\n      \"tabMenus\": [\n        {\n          \"items\": [\n            {\n              \"subLinks\": [],\n              \"icon\": \"custom custom-gonative-icon\",\n              \"label\": \"Home\",\n              \"url\": \"https://gonative.io\"\n            },\n            {\n              \"subLinks\": [],\n              \"icon\": \"fas fa-globe\",\n              \"label\": \"Tab 2\",\n              \"url\": \"javascript:alert('You selected tab 2. These tabs are only shown on the home page')\"\n            },\n            {\n              \"subLinks\": [],\n              \"icon\": \"fas fa-laptop-code\",\n              \"label\": \"GoNative Demo\",\n              \"url\": \"https://gonative.io/demo\"\n            }\n          ],\n          \"id\": \"1\"\n        }\n      ],\n      \"active\": true\n    },\n    \"sidebarNavigation\": {\n      \"menuSelectionConfig\": {\n        \"testURL\": null,\n        \"redirectLocations\": [\n          {\n            \"regex\": null,\n            \"menuName\": \"default\",\n            \"loggedIn\": false\n          },\n          {\n            \"regex\": \".*\",\n            \"menuName\": \"default\",\n            \"loggedIn\": true\n          }\n        ]\n      },\n      \"menus\": [\n        {\n          \"items\": [\n            {\n              \"subLinks\": [],\n              \"label\": \"Sample Home\",\n              \"url\": \"https://gonative.io\",\n              \"icon\": \"fas fa-home\"\n            },\n            {\n              \"subLinks\": [],\n              \"label\": \"Sample About\",\n              \"url\": \"https://gonative.io/about\",\n              \"icon\": \"fas fa-user\"\n            }\n          ],\n          \"name\": \"default\",\n          \"active\": true\n        },\n        {\n          \"items\": null,\n          \"name\": \"loggedIn\",\n          \"active\": false\n        }\n      ]\n    },\n    \"regexInternalExternal\": {\n      \"rules\": [\n        {\n          \"regex\": \"^(?!https?://).*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/login.php.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/dialog.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/v1\\\\.0/.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/oauth.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/v2\\\\.0/.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com/checkpoint.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*facebook\\\\.com.*\",\n          \"internal\": true\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*plus\\\\.google\\\\.com/share.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*twitter\\\\.com/.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*instagram\\\\.com/.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://maps\\\\.google\\\\.com.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*linkedin\\\\.com/.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \"https?://([-\\\\w]+\\\\.)*google\\\\.com/maps/search/.*\",\n          \"internal\": false\n        },\n        {\n          \"regex\": \".*\",\n          \"internal\": true\n        }\n      ],\n      \"active\": false\n    },\n    \"androidPullToRefresh\": false,\n    \"iosPullToRefresh\": true,\n    \"navigationTitles\": {\n      \"titles\": [\n        {}\n      ],\n      \"active\": false\n    },\n    \"toolbarNavigation\": {\n      \"items\": [\n        {\n          \"system\": \"back\",\n          \"title\": \"Back\"\n        },\n        {\n          \"system\": \"forward\",\n          \"title\": \"Forward\"\n        },\n        {\n          \"system\": \"refresh\"\n        }\n      ]\n    },\n    \"androidShowRefreshButton\": false,\n    \"deepLinkDomains\": {\n      \"domains\": [],\n      \"enableAndroidApplinks\": false\n    },\n    \"navigationLevels\": {\n      \"levels\": [\n        {}\n      ]\n    }\n  },\n  \"styling\": {\n    \"transitionInteractiveDelayMax\": 0.2,\n    \"menuAnimationDuration\": 0.15,\n    \"androidShowSplash\": true,\n    \"disableAnimations\": false,\n    \"hideWebviewAlpha\": 0.5,\n    \"showActionBar\": true,\n    \"showNavigationBar\": true,\n    \"iosSidebarFont\": \"Default\",\n    \"androidHideTitleInActionBar\": true,\n    \"navigationTitleImage\": true,\n    \"iosTheme\": \"default\",\n    \"androidTheme\": \"auto\",\n    \"androidSidebarBackgroundColor\": \"#FFFFFF\",\n    \"androidSidebarForegroundColor\": \"#1E496E\",\n    \"androidActionBarBackgroundColor\": \"#FFFFFF\",\n    \"androidActionBarForegroundColor\": \"#1E496E\",\n    \"androidAccentColor\": \"#1E496E\",\n    \"androidSidebarSeparatorColor\": \"#CCCCCC\",\n    \"androidSidebarHighlightColor\": \"#1E496E\",\n    \"androidShowLogoInSideBar\": true,\n    \"androidShowAppNameInSideBar\": true,\n    \"androidPullToRefreshColor\": \"#1E496E\",\n    \"androidTabBarBackgroundColor\": \"#FFFFFF\",\n    \"androidTabBarTextColor\": \"#949494\",\n    \"androidTabBarIndicatorColor\": \"#1E496E\",\n    \"androidStatusBarBackgroundColor\": \"#5C5C5C\",\n    \"iosTintColor\": \"#1E496E\",\n    \"iosTitleColor\": \"#1E496E\",\n    \"iosSidebarTextColor\": \"#1E496E\",\n    \"androidBackgroundColor\": \"#FFFFFF\",\n    \"androidSwipeNavigationBackgroundColor\": \"#FFFFFF\",\n    \"androidSwipeNavigationActiveColor\": \"#000000\",\n    \"androidSwipeNavigationInactiveColor\": \"#666666\",\n    \"androidActionBarBackgroundColorDark\": \"#333333\",\n    \"androidStatusBarBackgroundColorDark\": \"#333333\",\n    \"androidActionBarForegroundColorDark\": \"#FFFFFF\",\n    \"androidAccentColorDark\": \"#666666\",\n    \"androidBackgroundColorDark\": \"#333333\",\n    \"androidSidebarForegroundColorDark\": \"#FFFFFF\",\n    \"androidSidebarBackgroundColorDark\": \"#333333\",\n    \"androidSidebarSeparatorColorDark\": \"#666666\",\n    \"androidSidebarHighlightColorDark\": \"#FFFFFF\",\n    \"androidPullToRefreshColorDark\": \"#FFFFFF\",\n    \"androidTabBarTextColorDark\": \"#FFFFFF\",\n    \"androidTabBarBackgroundColorDark\": \"#333333\",\n    \"androidTabBarIndicatorColorDark\": \"#666666\",\n    \"androidSwipeNavigationBackgroundColorDark\": \"#333333\",\n    \"androidSwipeNavigationActiveColorDark\": \"#FFFFFF\",\n    \"androidSwipeNavigationInactiveColorDark\": \"#666666\"\n  },\n  \"permissions\": {\n    \"usesGeolocation\": false,\n    \"androidDownloadToPublicStorage\": true,\n    \"enableWebRTC\": false\n  },\n  \"performance\": {\n    \"webviewPools\": [\n      {\n        \"urls\": [\n          {\n            \"disown\": \"reload\"\n          }\n        ]\n      }\n    ]\n  },\n  \"services\": {\n    \"facebook\": {\n      \"pluginName\": \"\"\n    }\n  }\n}"
  },
  {
    "path": "app/src/main/assets/custom-icons.json",
    "content": "{\n  \"gonative-icon\": 59392\n}\n"
  },
  {
    "path": "app/src/main/assets/offline.html",
    "content": "<html>\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, user-scalable=no\">\n    <title>Device Offline</title>\n    <style type=\"text/css\">\n      :root { --background-color: white; --primary-color: #1C1C1E; }\n      [data-theme=\"dark\"] { --background-color: #121212; --primary-color: #eee; }\n      [data-theme=\"light\"] { --background-color: white; --primary-color: #1C1C1E; }\n      html, body{ background-color: var(--background-color) !important; }\n      html, body, button { font-family: Arial, Helvetica, sans-serif; font-size: 18px; }\n      div.container { color: var(--primary-color); position: relative; top: 100px; text-align: center; }\n      #logo>svg>g>path{ fill: var(--primary-color); }\n      button { padding: 10px 30px; margin: 4px 2px; }\n    </style>\n</head>\n<body>\n<div class=\"container\">\n    <div id=\"logo\">\n        <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>\n    </div>\n    <span id=\"message\">\n        <p>No internet connection<br/>Check your connection and try again</p>\n      </span>\n    <button id=\"retryButton\" type=\"button\" onclick=\"gonative.webview.reload()\">Retry</button>\n</div>\n<script>\n      var message = document.getElementById('message');\n      var retryButton = document.getElementById('retryButton')\n      if (['ko', 'ko-kr', 'ko-kp'].indexOf(navigator.language.toLowerCase()) > -1) {\n        // Korean\n        message.innerHTML = '<p>인터넷 연결이 끊겼습니다.<br/>연결하고 다시 시도하시길 바랍니다.</p>';\n        retryButton.innerText = '괜찮아';\n      }\n      function updateDarkMode(){\n        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {\n          document.documentElement.setAttribute(\"data-theme\", \"dark\");\n        }\n        else {\n          document.documentElement.setAttribute(\"data-theme\", \"light\");\n        }\n      }\n      updateDarkMode();\n      if(window.matchMedia){\n        window.matchMedia('(prefers-color-scheme: dark)')\n          .addEventListener('change', updateDarkMode);\n      }\n    </script>\n</body>\n</html>"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ActionManager.java",
    "content": "package io.gonative.android;\n\nimport android.graphics.Color;\nimport android.graphics.PorterDuff;\nimport android.graphics.Typeface;\nimport android.graphics.drawable.Drawable;\nimport android.text.TextUtils;\nimport android.view.LayoutInflater;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.Button;\nimport android.widget.ImageView;\nimport android.widget.LinearLayout;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.ActionBarDrawerToggle;\nimport androidx.appcompat.widget.LinearLayoutCompat;\nimport androidx.appcompat.widget.SearchView;\nimport androidx.core.content.ContextCompat;\nimport androidx.drawerlayout.widget.DrawerLayout;\n\nimport com.google.android.material.appbar.MaterialToolbar;\n\nimport io.gonative.android.icons.Icon;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.UnsupportedEncodingException;\nimport java.net.URLEncoder;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.regex.Pattern;\n\nimport io.gonative.gonative_core.AppConfig;\n\n/**\n * Created by weiyin on 11/25/14.\n * Copyright 2014 GoNative.io LLC\n */\npublic class ActionManager {\n    private static final String TAG = ActionManager.class.getName();\n    private static final String ACTION_SHARE = \"share\";\n    private static final String ACTION_REFRESH = \"refresh\";\n    private static final String ACTION_SEARCH = \"search\";\n    private static final int ACTIONBAR_ITEM_MARGIN = 132;\n\n    private final MainActivity activity;\n    private final HashMap<MenuItem, String>itemToUrl;\n    private final int action_button_size;\n    private final ActionBar actionBar;\n    private final ImageView actionBarImageTitle;\n    private final int colorForeground;\n    private final int colorBackground;\n    private boolean isRoot;\n    private String currentMenuID;\n    private LinearLayout header;\n    private LinearLayoutCompat menuContainer;\n    private RelativeLayout titleContainer;\n    private boolean isOnSearchMode = false;\n    private SearchView searchView;\n\n    private int leftItemsCount = 0;\n    private int rightItemsCount = 0;\n\n    private String currentUrl;\n\n    ActionManager(MainActivity activity) {\n        this.activity = activity;\n        this.itemToUrl = new HashMap<>();\n        this.action_button_size = this.activity.getResources().getInteger(R.integer.action_button_size);\n        this.actionBar = activity.getSupportActionBar();\n\n        this.actionBarImageTitle = new ImageView(activity);\n        this.actionBarImageTitle.setImageResource(R.drawable.ic_actionbar);\n\n        this.colorForeground = activity.getResources().getColor(R.color.titleTextColor);\n        this.colorBackground = activity.getResources().getColor(R.color.colorPrimary);\n    }\n\n    public void setupActionBar(boolean isRoot) {\n        if (actionBar == null) return;\n\n        this.isRoot = isRoot;\n        AppConfig appConfig = AppConfig.getInstance(activity);\n\n        // Change hamburger button to back arrow if window is not root\n        if (!isRoot) {\n            actionBar.setDisplayHomeAsUpEnabled(true);\n            Drawable backArrow = ContextCompat.getDrawable(activity, R.drawable.abc_ic_ab_back_material);\n            backArrow.setColorFilter(colorForeground, PorterDuff.Mode.SRC_ATOP);\n            actionBar.setHomeAsUpIndicator(backArrow);\n        }\n\n        header = (LinearLayout) activity.getLayoutInflater().inflate(R.layout.actionbar_title, null);\n        // why use a custom view and not setDisplayUseLogoEnabled and setLogo?\n        // Because logo doesn't work!\n        actionBar.setDisplayShowCustomEnabled(true);\n        actionBar.setDisplayShowTitleEnabled(false);\n        actionBar.setCustomView(header);\n        ActionBar.LayoutParams params = (ActionBar.LayoutParams) header.getLayoutParams();\n        params.width = ActionBar.LayoutParams.MATCH_PARENT;\n        titleContainer = header.findViewById(R.id.title_container);\n\n        menuContainer = header.findViewById(R.id.left_menu_container);\n\n        ViewGroup.MarginLayoutParams titleContainerParams = (ViewGroup.MarginLayoutParams) titleContainer.getLayoutParams();\n        titleContainerParams.rightMargin = ACTIONBAR_ITEM_MARGIN + 8;\n\n        MaterialToolbar toolbar = activity.findViewById(R.id.toolbar);\n        toolbar.setBackgroundColor(colorBackground);\n\n        // fix title offset when side menu hamburger icon is not yet available\n        if (appConfig.showNavigationMenu && isRoot) {\n            menuContainer.getLayoutParams().width = 140;\n        }\n    }\n\n    public void setupTitleDisplayForUrl(String url) {\n        if (actionBar == null || url == null) return;\n        AppConfig appConfig = AppConfig.getInstance(activity);\n        this.currentUrl = url;\n\n        boolean urlHasNavTitle = false;\n        boolean urlHasActionMenu = false;\n\n        // Check for Nav title\n        HashMap<String, Object> urlNavTitle = appConfig.getNavigationTitleForUrl(url);\n        if (urlNavTitle != null) {\n            urlHasNavTitle = true;\n        }\n\n        // Check for Action Menus\n        ArrayList<Pattern> regexes = appConfig.actionRegexes;\n        ArrayList<String> ids = appConfig.actionIDs;\n        if (regexes != null && ids != null) {\n            for (int i = 0; i < regexes.size(); i++) {\n                Pattern regex = regexes.get(i);\n                if (regex.matcher(url).matches()) {\n                    urlHasActionMenu = true;\n                    break;\n                }\n            }\n        }\n\n        if (!appConfig.showActionBar && !appConfig.showNavigationMenu && !urlHasNavTitle && !urlHasActionMenu) {\n            actionBar.hide();\n        } else {\n            if (urlHasNavTitle) {\n                boolean showImage = true;\n                if (urlNavTitle.containsKey(\"showImage\")) showImage = (boolean) urlNavTitle.get(\"showImage\");\n\n                if (showImage) {\n                    // Show image title\n                    actionBar.setDisplayOptions(0, ActionBar.DISPLAY_SHOW_TITLE);\n                    showTitleView(actionBarImageTitle);\n                } else {\n                    // Show text title\n                    String title = activity.getTitle().toString();;\n                    if (urlNavTitle.containsKey(\"title\")) title = (String) urlNavTitle.get(\"title\");\n                    showTextActionBarTitle(title);\n                }\n            } else {\n                showLogoInActionBar(appConfig.shouldShowNavigationTitleImageForUrl(currentUrl));\n            }\n            setupActionBarDisplay();\n            actionBar.show();\n        }\n    }\n\n    private void showLogoInActionBar(boolean show) {\n        if (actionBar == null) return;\n        if (show) {\n            showTitleView(actionBarImageTitle);\n        } else {\n            // Show Text\n            showTextActionBarTitle(activity.getTitle());\n        }\n    }\n\n    public void showTextActionBarTitle(CharSequence title) {\n        TextView textView = new TextView(activity);\n        textView.setText(TextUtils.isEmpty(title) ? activity.getTitle() : title);\n        textView.setTextSize(18);\n        textView.setTypeface(null, Typeface.BOLD);\n        textView.setMaxLines(1);\n        textView.setEllipsize(TextUtils.TruncateAt.END);\n        textView.setTextColor(colorForeground);\n        showTitleView(textView);\n    }\n\n    public void showTitleView(View titleView) {\n        if (actionBar == null) return;\n        if (titleView == null) return;\n\n        LinearLayout header = (LinearLayout) actionBar.getCustomView();\n\n        if (header == null) return;\n\n        // Remove Title Container child views if there is any\n        titleContainer.removeAllViews();\n\n        // Remove Title View parent if there is any\n        if (titleView.getParent() != null) {\n            ((ViewGroup) titleView.getParent()).removeView(titleView);\n        }\n\n        titleContainer.addView(titleView);\n    }\n\n    // Remove title offset once sidebar hamburger menu setup is complete\n    public void cleanSidebarMenuTitleOffset() {\n        if (menuContainer == null) return;\n        menuContainer.getLayoutParams().width = 0;\n    }\n\n    public void checkActions(String url) {\n        if (this.activity == null || url == null) return;\n\n        AppConfig appConfig = AppConfig.getInstance(this.activity);\n\n        ArrayList<Pattern> regexes = appConfig.actionRegexes;\n        ArrayList<String> ids = appConfig.actionIDs;\n        if (regexes == null || ids == null) {\n            setMenuID(null);\n            return;\n        }\n\n        for (int i = 0; i < regexes.size(); i++) {\n            Pattern regex = regexes.get(i);\n            if (regex.matcher(url).matches()) {\n                setMenuID(ids.get(i));\n                return;\n            }\n        }\n\n        setMenuID(null);\n    }\n\n    private void setMenuID(String menuID) {\n        boolean changed;\n\n        if (this.currentMenuID == null) {\n            changed = menuID != null;\n        } else {\n            changed = menuID == null || !this.currentMenuID.equals(menuID);\n        }\n\n        if (changed) {\n            this.currentMenuID = menuID;\n            this.activity.invalidateOptionsMenu();\n        }\n    }\n\n    public void addActions(Menu menu) {\n        this.itemToUrl.clear();\n        this.rightItemsCount = 0;\n        this.leftItemsCount = 0;\n\n        AppConfig appConfig = AppConfig.getInstance(this.activity);\n        if (appConfig.actions == null) return;\n        menuContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;\n\n        JSONArray actions = appConfig.actions.get(currentMenuID);\n        if (actions == null || actions.length() == 0) {\n            replaceLeftIcon(null);\n        } else {\n            if (actions.length() <= 2) {\n                for (int itemID = 0; itemID < actions.length(); itemID++) {\n                    JSONObject entry = actions.optJSONObject(itemID);\n                    addRightButton(appConfig, menu, itemID, entry);\n                }\n            } else {\n                for (int itemID = 0; itemID < actions.length(); itemID++) {\n                    JSONObject entry = actions.optJSONObject(itemID);\n                    if (itemID == 0) {\n                        addLeftButton(appConfig, entry);\n                    } else {\n                        addRightButton(appConfig, menu, itemID, entry);\n                    }\n                }\n            }\n        }\n        setupActionBarDisplay();\n    }\n\n    private void addLeftButton(AppConfig appConfig, JSONObject entry) {\n        if (entry == null) return;\n\n        String system = AppConfig.optString(entry, \"system\");\n        String icon = AppConfig.optString(entry, \"icon\");\n        String url = AppConfig.optString(entry, \"url\");\n\n        if (!TextUtils.isEmpty(system)) {\n            if (system.equalsIgnoreCase(\"refresh\")) {\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa-rotate-right\";\n                }\n                Button refresh = createButtonMenu(icon);\n                refresh.setOnClickListener(v -> this.activity.onRefresh());\n                replaceLeftIcon(refresh);\n            } else if (system.equalsIgnoreCase(\"share\")) {\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa-share\";\n                }\n                Button share = createButtonMenu(icon);\n                share.setOnClickListener(v -> this.activity.sharePage(null, null));\n                replaceLeftIcon(share);\n            } else if (system.equalsIgnoreCase(\"search\")) {\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa fa-search\";\n                }\n                this.searchView = createSearchView(appConfig, icon, url, null, true);\n                replaceLeftIcon(this.searchView);\n            } else {\n                addLeftCustomButton(icon, url);\n            }\n        } else {\n            addLeftCustomButton(icon, url);\n        }\n\n        if (!appConfig.showNavigationMenu) {\n            ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) menuContainer.getLayoutParams();\n            params.leftMargin = 35;\n        }\n\n        leftItemsCount++;\n    }\n\n    private void addLeftCustomButton(String icon, String url) {\n        Button userButton = createButtonMenu(icon);\n        userButton.setOnClickListener(v -> this.activity.loadUrl(url));\n        replaceLeftIcon(userButton);\n    }\n\n    private void addRightButton(AppConfig appConfig, Menu menu, int itemID, JSONObject entry) {\n        if (entry == null) return;\n\n        String system = AppConfig.optString(entry, \"system\");\n        String label = AppConfig.optString(entry, \"label\");\n        String icon = AppConfig.optString(entry, \"icon\");\n        String url = AppConfig.optString(entry, \"url\");\n\n        if (!TextUtils.isEmpty(system)) {\n            if (system.equalsIgnoreCase(\"refresh\")) {\n\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa-rotate-right\";\n                }\n\n                Drawable refreshIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();\n\n                String menuLabel = !TextUtils.isEmpty(label) ? label : \"Refresh\";\n\n                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)\n                        .setIcon(refreshIcon)\n                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);\n\n                itemToUrl.put(menuItem, ACTION_REFRESH);\n            } else if (system.equalsIgnoreCase(\"share\")) {\n\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa-share\";\n                }\n\n                Drawable refreshIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();;\n\n                String menuLabel = !TextUtils.isEmpty(label) ? label : \"Share\";\n\n                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)\n                        .setIcon(refreshIcon)\n                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);\n\n                itemToUrl.put(menuItem, ACTION_SHARE);\n\n            } else if (system.equalsIgnoreCase(\"search\")) {\n\n                if (TextUtils.isEmpty(icon)) {\n                    icon = \"fa fa-search\";\n                }\n\n                String menuLabel = !TextUtils.isEmpty(label) ? label : \"Search\";\n\n                MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, menuLabel)\n                        .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);\n\n                this.searchView = createSearchView(appConfig, icon, url, menuItem, false);\n                menuItem.setActionView(searchView);\n\n                itemToUrl.put(menuItem, ACTION_SEARCH);\n            } else {\n                addRightCustomButton(menu, itemID, label, icon, url);\n            }\n        } else {\n            addRightCustomButton(menu, itemID, label, icon, url);\n        }\n\n        rightItemsCount++;\n    }\n\n    private void addRightCustomButton(Menu menu, int itemID, String label, String icon, String url) {\n        Drawable iconDrawable = null;\n        if (icon != null) {\n            iconDrawable = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();\n        }\n        MenuItem menuItem = menu.add(Menu.NONE, itemID, Menu.NONE, label)\n                .setIcon(iconDrawable)\n                .setShowAsActionFlags(MenuItem.SHOW_AS_ACTION_IF_ROOM);\n\n        if (url != null) {\n            this.itemToUrl.put(menuItem, url);\n        }\n    }\n\n    private void replaceLeftIcon(View view) {\n        if (menuContainer == null) return;\n        menuContainer.removeAllViews();\n        if (view != null) {\n            menuContainer.addView(view);\n            menuContainer.setVisibility(View.VISIBLE);\n        } else {\n            menuContainer.setVisibility(View.GONE);\n        }\n    }\n\n    private Button createButtonMenu(String iconString) {\n        Drawable icon = new Icon(activity, iconString, action_button_size, colorForeground).getDrawable();\n        icon.setBounds(0, 0, 50, 50);\n        LinearLayout tempView = (LinearLayout) LayoutInflater.from(activity).inflate(R.layout.button_menu, null);\n        Button button = tempView.findViewById(R.id.menu_button);\n        tempView.removeView(button);\n        button.setCompoundDrawables(icon, null, null, null);\n        return button;\n    }\n\n    private SearchView createSearchView(AppConfig appConfig, String icon, String url, MenuItem menuItem, boolean forLeftSide) {\n        SearchView searchView = new SearchView(activity);\n\n        // Set layout Params to WRAP_CONTENT\n        ViewGroup.LayoutParams searchViewParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);\n        searchView.setLayoutParams(searchViewParams);\n\n        // Left Drawer Instance\n        DrawerLayout drawerLayout = activity.getDrawerLayout();\n        ActionBarDrawerToggle drawerToggle = activity.getDrawerToggle();\n\n        // search item in action bar\n        SearchView.SearchAutoComplete searchAutoComplete = searchView.findViewById(androidx.appcompat.R.id.search_src_text);\n        if (searchAutoComplete != null) {\n            searchAutoComplete.setTextColor(colorForeground);\n            int hintColor = colorForeground;\n            hintColor = Color.argb(192, Color.red(hintColor), Color.green(hintColor),\n                    Color.blue(hintColor));\n            searchAutoComplete.setHintTextColor(hintColor);\n        }\n\n        searchView.setOnSearchClickListener(view -> {\n            searchViewParams.width = ActionBar.LayoutParams.MATCH_PARENT;\n\n            // Need to check this otherwise the app will crash\n            if (!activity.isNotRoot() && appConfig.showNavigationMenu) {\n                drawerToggle.setDrawerIndicatorEnabled(false);\n                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);\n\n                drawerToggle.setDrawerIndicatorEnabled(false);\n                actionBar.setDisplayShowHomeEnabled(true);\n            } else if (!activity.isNotRoot()) {\n                actionBar.setDisplayHomeAsUpEnabled(true);\n            }\n            isOnSearchMode = true;\n        });\n\n        searchView.setOnCloseListener(() -> {\n            if (forLeftSide) {\n                titleContainer.setVisibility(View.VISIBLE);\n            } else {\n                header.setVisibility(View.VISIBLE);\n                activity.invalidateOptionsMenu();\n            }\n            searchViewParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;\n\n            activity.setMenuItemsVisible(true);\n            if (!activity.isNotRoot() && appConfig.showNavigationMenu) {\n                drawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);\n                actionBar.setDisplayShowHomeEnabled(false);\n                drawerToggle.setDrawerIndicatorEnabled(true);\n            } else if (!activity.isNotRoot()) {\n                actionBar.setDisplayHomeAsUpEnabled(false);\n            }\n            return false;\n        });\n\n        // listener to process query\n        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {\n            @Override\n            public boolean onQueryTextSubmit(String query) {\n                if (!searchView.isIconified()) {\n                    searchView.setIconified(true);\n                }\n                try {\n                    String q = URLEncoder.encode(query, \"UTF-8\");\n                    activity.loadUrl(url + q);\n                } catch (UnsupportedEncodingException e) {\n                    return true;\n                }\n\n                return true;\n            }\n\n            @Override\n            public boolean onQueryTextChange(String newText) {\n                // do nothing\n                return true;\n            }\n        });\n\n        // listener to collapse action view when soft keyboard is closed\n        searchView.setOnQueryTextFocusChangeListener(new View.OnFocusChangeListener() {\n            @Override\n            public void onFocusChange(View v, boolean hasFocus) {\n                if (!hasFocus) {\n                    if (!searchView.isIconified()) {\n                        searchView.setIconified(true);\n                    }\n                }\n            }\n        });\n\n        // Search view button icon and color\n        ImageView searchIcon = searchView.findViewById(androidx.appcompat.R.id.search_button);\n        if (searchIcon != null) {\n            icon = !TextUtils.isEmpty(icon) ? icon : \"fa fa-search\";\n            Drawable searchButtonNewIcon = new Icon(activity, icon, action_button_size, colorForeground).getDrawable();\n            searchIcon.setImageDrawable(searchButtonNewIcon);\n            searchIcon.setColorFilter(colorForeground);\n\n            // Handling a bug when SearchView is expanded and setting other menu\n            // visibility to false, SearchView would trigger unnecessary onClose event\n            // Solution is to hide other menus first before expanding\n            searchIcon.setOnClickListener(v -> {\n                if (forLeftSide) {\n                    activity.setMenuItemsVisible(false);\n                    titleContainer.setVisibility(View.GONE);\n                } else {\n                    header.setVisibility(View.GONE);\n                    activity.setMenuItemsVisible(false, menuItem);\n                }\n                // Expand SearchView, simulates onSearchViewClicked event\n                searchView.setIconified(false);\n            });\n        }\n\n        //Search view close button foreground color\n        ImageView closeButtonImage = searchView.findViewById(androidx.appcompat.R.id.search_close_btn);\n        if (closeButtonImage != null) {\n            closeButtonImage.setColorFilter(colorForeground);\n        }\n\n        return searchView;\n    }\n\n    // Count left and right actionbar buttons to calculate side margins\n    public void setupActionBarDisplay() {\n\n        if (actionBar == null) return;\n\n        AppConfig appConfig = AppConfig.getInstance(activity);\n\n        // Add to temporary fields so actual items count would not be affected\n        int tempLeftItemsCount = leftItemsCount;\n\n        // Limit right menu count to three for margin\n        int tempRightItemsCount = Math.min(rightItemsCount, 3);\n\n        ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) titleContainer.getLayoutParams();\n\n        // Reset the margins\n        params.rightMargin = 0;\n        params.leftMargin = 0;\n\n        if (isRoot) {\n            if (appConfig.showNavigationMenu) {\n                tempLeftItemsCount++;\n            }\n\n            if (tempLeftItemsCount > tempRightItemsCount) {\n                int margin = tempLeftItemsCount - tempRightItemsCount;\n                params.rightMargin = ACTIONBAR_ITEM_MARGIN * margin;\n            } else {\n                int margin = tempRightItemsCount - tempLeftItemsCount;\n                params.leftMargin = ACTIONBAR_ITEM_MARGIN * margin;\n            }\n        } else {\n            tempLeftItemsCount++;\n\n            if (tempLeftItemsCount > tempRightItemsCount) {\n                int margin = tempLeftItemsCount - tempRightItemsCount;\n                params.rightMargin = ACTIONBAR_ITEM_MARGIN * margin;\n            } else {\n                int margin = tempRightItemsCount - tempLeftItemsCount;\n                params.leftMargin = ACTIONBAR_ITEM_MARGIN * margin;\n            }\n        }\n    }\n\n    public boolean isOnSearchMode() {\n        return isOnSearchMode;\n    }\n\n    public void setOnSearchMode(boolean onSearchMode) {\n        isOnSearchMode = onSearchMode;\n    }\n\n    public void closeSearchView() {\n        if (searchView == null) return;\n\n        if (!searchView.isIconified()) {\n            searchView.setIconified(true);\n        }\n    }\n\n    public boolean onOptionsItemSelected(MenuItem item) {\n        if (activity.getCurrentFocus() instanceof SearchView.SearchAutoComplete) {\n            activity.getCurrentFocus().clearFocus();\n        }\n        String url = this.itemToUrl.get(item);\n        if (url != null) {\n            switch (url) {\n                case ACTION_SHARE:\n                    this.activity.sharePage(null, null);\n                    return true;\n                case ACTION_REFRESH:\n                    this.activity.onRefresh();\n                    return true;\n                case ACTION_SEARCH:\n                    // Ignore\n                    return true;\n            }\n\n            this.activity.loadUrl(url);\n            return true;\n        } else {\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/AppLinksActivity.java",
    "content": "package io.gonative.android;\n\nimport android.content.Intent;\nimport android.os.Bundle;\n\nimport androidx.annotation.Nullable;\nimport androidx.appcompat.app.AppCompatActivity;\n\npublic class AppLinksActivity extends AppCompatActivity {\n\n    @Override\n    protected void onCreate(@Nullable Bundle savedInstanceState) {\n        super.onCreate(savedInstanceState);\n        launchApp();\n    }\n\n    private void launchApp() {\n        Intent intent = new Intent(this, MainActivity.class);\n        if (getIntent().getData() != null) {\n            intent.setData(getIntent().getData());\n            intent.setAction(Intent.ACTION_VIEW);\n        }\n        startActivity(intent);\n        finish();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/AudioUtils.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.media.AudioAttributes;\nimport android.media.AudioFocusRequest;\nimport android.media.AudioManager;\nimport android.util.Log;\n\npublic class AudioUtils {\n    private static final String TAG = AudioUtils.class.getName();\n    private static AudioFocusRequest initialFocusRequest;\n    private static AudioFocusRequest focusRequest;\n    private static AudioManager.OnAudioFocusChangeListener initialAudioFocusChangeListener;\n    private static AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;\n    \n    /**\n     * @param mode - Accepts int for speaker mode:\n     *             0 - phone speaker (default)\n     *             1 - headset / wired device\n     *             2 - bluetooth\n     */\n    public static void setUpAudioDevice(MainActivity mainActivity, int mode) {\n        AudioManager mAudioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);\n        if (mode == 2) {\n            // bluetooth device\n            mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);\n            mAudioManager.startBluetoothSco();\n            mAudioManager.setBluetoothScoOn(true);\n        } else if (mode == 1) {\n            // wired device\n            mAudioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);\n            mAudioManager.stopBluetoothSco();\n            mAudioManager.setBluetoothScoOn(false);\n            mAudioManager.setSpeakerphoneOn(false);\n        } else {\n            // phone speaker\n            mAudioManager.setMode(AudioManager.MODE_NORMAL);\n            mAudioManager.stopBluetoothSco();\n            mAudioManager.setBluetoothScoOn(false);\n            mAudioManager.setSpeakerphoneOn(true);\n        }\n    }\n    \n    public static void reconnectToBluetooth(MainActivity mainActivity, AudioManager audioManager) {\n        if (audioManager.isBluetoothScoAvailableOffCall() && !audioManager.isBluetoothScoOn()) {\n            Log.d(TAG, \"Resetting audio to bluetooth device\");\n            setUpAudioDevice(mainActivity, 2);\n        }\n    }\n    \n    /**\n     * Listen to the first AUDIOFOCUS_GAIN before taking the audio input/output priority through requestAudioFocus()\n     *\n     * @param mainActivity\n     */\n    public static void initAudioFocusListener(MainActivity mainActivity) {\n        int result;\n        final Object focusLock = new Object();\n        \n        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);\n        if (audioManager == null) {\n            Log.w(TAG, \"AudioManager is null. Aborting initAudioFocusListener()\");\n        }\n        \n        initialAudioFocusChangeListener = focusChange -> {\n            if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {\n                synchronized (focusLock) {\n                    Log.d(TAG, \"AudioFocusListener GAINED. Try to request audio focus\");\n                    requestAudioFocus(mainActivity);\n                    abandonFocusRequest(mainActivity);\n                }\n            }\n        };\n        \n        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {\n            result = audioManager.requestAudioFocus(initialAudioFocusChangeListener,\n                    AudioManager.STREAM_VOICE_CALL,\n                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);\n        } else {\n            AudioAttributes playbackAttributes = new AudioAttributes.Builder()\n                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)\n                    .build();\n            initialFocusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)\n                    .setAudioAttributes(playbackAttributes)\n                    .setAcceptsDelayedFocusGain(true)\n                    .setOnAudioFocusChangeListener(initialAudioFocusChangeListener)\n                    .build();\n            result = audioManager.requestAudioFocus(initialFocusRequest);\n        }\n        \n        synchronized (focusLock) {\n            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {\n                Log.d(TAG, \"AudioFocusListener REQUEST GRANTED\");\n            }\n        }\n    }\n    \n    /**\n     * Prioritizes the bluetooth device if available.\n     * Reconnects the bluetooth device when the audio focus is lost as a workaround for aborted connections\n     * due to AudioRecord.AUDIO_INPUT_FLAG_FAST denial.\n     *\n     * @param mainActivity\n     */\n    public static void requestAudioFocus(MainActivity mainActivity) {\n        int result;\n        final Object focusLock = new Object();\n        \n        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);\n        if (audioManager == null) {\n            Log.w(TAG, \"AudioManager is null. Aborting requestAudioFocus()\");\n        }\n        audioFocusChangeListener = focusChange -> {\n            switch (focusChange) {\n                case AudioManager.AUDIOFOCUS_GAIN:\n                    synchronized (focusLock) {\n                        Log.d(TAG, \"AudioFocus GAINED. Try to connect bluetooth device\");\n                        reconnectToBluetooth(mainActivity, audioManager);\n                    }\n                    break;\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:\n                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:\n                case AudioManager.AUDIOFOCUS_LOSS:\n                    synchronized (focusLock) {\n                        Log.d(TAG, \"AudioFocus LOST. Try to reconnect bluetooth device\");\n                        reconnectToBluetooth(mainActivity, audioManager);\n                    }\n                    break;\n            }\n        };\n        \n        abandonFocusRequest(mainActivity);\n        \n        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {\n            result = audioManager.requestAudioFocus(audioFocusChangeListener,\n                    AudioManager.STREAM_VOICE_CALL,\n                    AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);\n        } else {\n            AudioAttributes playbackAttributes = new AudioAttributes.Builder()\n                    .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)\n                    .build();\n            focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)\n                    .setAudioAttributes(playbackAttributes)\n                    .setAcceptsDelayedFocusGain(true)\n                    .setOnAudioFocusChangeListener(audioFocusChangeListener)\n                    .build();\n            result = audioManager.requestAudioFocus(focusRequest);\n        }\n        \n        synchronized (focusLock) {\n            if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {\n                Log.d(TAG, \"AudioFocus REQUEST GRANTED\");\n                reconnectToBluetooth(mainActivity, audioManager);\n            }\n        }\n    }\n    \n    public static void abandonFocusRequest(MainActivity mainActivity) {\n        AudioManager audioManager = (AudioManager) mainActivity.getSystemService(Context.AUDIO_SERVICE);\n        if (audioManager == null) {\n            Log.w(TAG, \"AudioManager is null. Aborting abandonFocusRequest()\");\n        }\n        if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O) {\n            if (initialAudioFocusChangeListener != null) {\n                audioManager.abandonAudioFocus(initialAudioFocusChangeListener);\n                initialAudioFocusChangeListener = null;\n            }\n            if (audioFocusChangeListener != null) {\n                audioManager.abandonAudioFocus(audioFocusChangeListener);\n                audioFocusChangeListener = null;\n            }\n        } else {\n            if (initialFocusRequest != null) {\n                audioManager.abandonAudioFocusRequest(initialFocusRequest);\n                initialFocusRequest = null;\n            }\n            if (focusRequest != null) {\n                audioManager.abandonAudioFocusRequest(focusRequest);\n                focusRequest = null;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ConfigPreferences.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.content.SharedPreferences;\nimport android.preference.PreferenceManager;\nimport android.text.TextUtils;\n\npublic class ConfigPreferences {\n    private static final String APP_THEME_KEY = \"io.gonative.android.appTheme\";\n\n    private Context context;\n    private SharedPreferences sharedPreferences;\n\n    public ConfigPreferences(Context context) {\n        this.context = context;\n    }\n\n    private SharedPreferences getSharedPreferences() {\n        if (this.sharedPreferences == null) {\n            this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this.context);\n        }\n        return this.sharedPreferences;\n    }\n\n    public String getAppTheme() {\n        SharedPreferences preferences = getSharedPreferences();\n        return preferences.getString(APP_THEME_KEY, null);\n    }\n\n    public void setAppTheme(String appTheme) {\n        SharedPreferences preferences = getSharedPreferences();\n        if (TextUtils.isEmpty(appTheme)) {\n            preferences.edit().remove(APP_THEME_KEY).commit();\n        } else {\n            preferences.edit().putString(APP_THEME_KEY, appTheme).commit();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ConfigUpdater.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.OutputStreamWriter;\nimport java.lang.ref.WeakReference;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 8/8/14.\n */\npublic class ConfigUpdater {\n    private static final String TAG = ConfigUpdater.class.getName();\n\n    private Context context;\n\n    ConfigUpdater(Context context) {\n        this.context = context;\n    }\n\n    public void registerEvent() {\n        if (AppConfig.getInstance(context).disableEventRecorder) return;\n\n        new EventTask(context).execute();\n    }\n\n    private static class EventTask extends AsyncTask<Void, Void, Void> {\n        WeakReference<Context> contextReference;\n\n        EventTask(Context context) {\n            this.contextReference = new WeakReference<>(context);\n        }\n\n        @Override\n        protected Void doInBackground(Void... params) {\n            Context context = contextReference.get();\n            if (context == null) return null;\n\n            JSONObject json = new JSONObject(Installation.getInfo(context));\n\n            try {\n                json.put(\"event\", \"launch\");\n            } catch (JSONException e) {\n                GNLog.getInstance().logError(TAG, e.getMessage(), e);\n                return null;\n            }\n\n            try {\n                URL url = new URL(\"https://events.gonative.io/api/events/new\");\n                HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n                connection.setRequestMethod(\"POST\");\n                connection.setRequestProperty(\"Content-Type\", \"application/json\");\n                connection.setDoOutput(true);\n                connection.setDoInput(false); // we do not care about response\n                OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), \"UTF-8\");\n                writer.write(json.toString());\n                writer.close();\n                connection.connect();\n                connection.getResponseCode();\n                connection.disconnect();\n            } catch (Exception e) {\n                GNLog.getInstance().logError(TAG, e.getMessage(), e);\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/CustomHeaders.java",
    "content": "package io.gonative.android;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Build;\nimport android.provider.Settings;\nimport android.util.Base64;\n\nimport java.io.UnsupportedEncodingException;\nimport java.util.HashMap;\nimport java.util.Map;\n\nimport io.gonative.gonative_core.AppConfig;\n\n/**\n * Created by weiyin on 5/1/17.\n */\n\npublic class CustomHeaders {\n    public static Map<String, String> getCustomHeaders(Context context) {\n        AppConfig appConfig = AppConfig.getInstance(context);\n        if (appConfig.customHeaders == null) return null;\n\n        HashMap<String, String> result = new HashMap<>();\n        for (Map.Entry<String, String> entry : appConfig.customHeaders.entrySet()) {\n            String key = entry.getKey();\n            String val;\n            try {\n                val = interpolateValues(context, entry.getValue());\n            } catch (UnsupportedEncodingException e) {\n                val = null;\n            }\n\n            if (key != null & val != null) {\n                result.put(key, val);\n            }\n        }\n\n        return result;\n    }\n\n    private static String interpolateValues(Context context, String value) throws UnsupportedEncodingException {\n        if (value == null) return null;\n\n        if (value.contains(\"%DEVICEID%\")) {\n            @SuppressLint(\"HardwareIds\")\n            String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);\n            if (androidId == null) androidId = \"\";\n            value = value.replace(\"%DEVICEID%\", androidId);\n        }\n\n        if (value.contains(\"%DEVICENAME64%\")) {\n            // base 64 encoded name\n            String manufacturer = Build.MANUFACTURER;\n            String model = Build.MODEL;\n            String name;\n            if (model.startsWith(manufacturer)) {\n                name = model;\n            } else {\n                name = manufacturer + \" \" + model;\n            }\n\n            String name64 = Base64.encodeToString(name.getBytes(\"UTF-8\"), Base64.NO_WRAP);\n            value = value.replace(\"%DEVICENAME64%\", name64);\n        }\n\n        return value;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/DownloadService.java",
    "content": "package io.gonative.android;\n\nimport android.app.Service;\nimport android.content.ActivityNotFoundException;\nimport android.content.ContentResolver;\nimport android.content.Intent;\nimport android.net.Uri;\nimport android.os.Binder;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.Handler;\nimport android.os.IBinder;\nimport android.os.Looper;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.webkit.MimeTypeMap;\nimport android.widget.Toast;\n\nimport androidx.annotation.Nullable;\nimport androidx.core.content.FileProvider;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.LeanUtils;\n\npublic class DownloadService extends Service {\n\n    private static final String TAG = \"DownloadService\";\n    private static final String EXTRA_DOWNLOAD_ID = \"download_id\";\n    private static final String ACTION_CANCEL_DOWNLOAD = \"action_cancel_download\";\n    private static final int BUFFER_SIZE = 4096;\n    private static final int timeout = 5; // in seconds\n    private final Handler handler = new Handler(Looper.getMainLooper());\n\n    private final Map<Integer, DownloadTask> downloadTasks = new HashMap<>();\n    private int downloadId = 0;\n    private String userAgent;\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        AppConfig appConfig = AppConfig.getInstance(this);\n        this.userAgent = appConfig.userAgent;\n    }\n\n    @Override\n    public int onStartCommand(Intent intent, int flags, int startId) {\n        if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) {\n            int id = intent.getIntExtra(EXTRA_DOWNLOAD_ID, 0);\n            cancelDownload(id);\n        }\n        return START_NOT_STICKY;\n    }\n\n    @Nullable\n    @Override\n    public IBinder onBind(Intent intent) {\n        return new DownloadBinder();\n    }\n\n    public class DownloadBinder extends Binder {\n        public DownloadService getService() {\n            return DownloadService.this;\n        }\n    }\n\n    public void startDownload(String url, String filename, String mimetype, boolean shouldSaveToGallery, boolean open, FileDownloader.DownloadLocation location) {\n        DownloadTask downloadTask = new DownloadTask(url, filename, mimetype, shouldSaveToGallery, open, location);\n        downloadTasks.put(downloadTask.getId(), downloadTask);\n        downloadTask.startDownload();\n    }\n\n    public void cancelDownload(int downloadId) {\n        DownloadTask downloadTask = downloadTasks.get(downloadId);\n        if (downloadTask != null && downloadTask.isDownloading()) {\n            downloadTask.cancelDownload();\n        }\n    }\n\n    public void handleDownloadUri(FileDownloader.DownloadLocation location, Uri uri, String mimeType, boolean shouldSaveToGallery, boolean open, String filename) {\n        if (uri == null) return;\n        if (location == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {\n\n            if (shouldSaveToGallery) {\n                addFileToGallery(uri);\n            }\n\n            if (open) {\n                viewFile(uri, mimeType);\n            } else {\n                handler.post(() -> {\n                    if (shouldSaveToGallery) {\n                        Toast.makeText(this, R.string.file_download_finished_gallery, Toast.LENGTH_SHORT).show();\n                    } else {\n                        Toast.makeText(this, String.format(this.getString(R.string.file_download_finished_with_name), filename), Toast.LENGTH_SHORT).show();\n                    }\n                });\n            }\n        } else {\n            if (open) {\n                viewFile(uri, mimeType);\n            } else {\n                handler.post(() -> Toast.makeText(this, String.format(this.getString(R.string.file_download_finished_with_name), filename), Toast.LENGTH_SHORT).show());\n            }\n        }\n    }\n\n    private void viewFile(Uri uri, String mimeType) {\n        try {\n            Intent intent = new Intent(Intent.ACTION_VIEW);\n            intent.setDataAndType(uri, mimeType);\n            intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);\n            startActivity(intent);\n        } catch (ActivityNotFoundException e) {\n            String message = getResources().getString(R.string.file_handler_not_found);\n            handler.post(() -> {\n                Toast.makeText(this, message, Toast.LENGTH_LONG).show();\n            });\n        } catch (Exception ex) {\n            GNLog.getInstance().logError(TAG, \"viewFile: Exception:\", ex);\n        }\n    }\n\n    private void addFileToGallery(Uri uri) {\n        Log.d(TAG, \"addFileToGallery: Adding to Albums . . .\");\n        Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);\n        mediaScanIntent.setData(uri);\n        sendBroadcast(mediaScanIntent);\n    }\n\n    private class DownloadTask {\n        private final int id;\n        private final String url;\n        private boolean isDownloading;\n        private HttpURLConnection connection;\n        private InputStream inputStream;\n        private FileOutputStream outputStream;\n        private File outputFile = null;\n        private Uri downloadUri;\n        private String filename;\n        private String extension;\n        private String mimetype;\n        private boolean saveToGallery;\n        private boolean openOnFinish;\n        private final FileDownloader.DownloadLocation location;\n\n        public DownloadTask(String url, String filename, String mimetype, boolean saveToGallery, boolean open, FileDownloader.DownloadLocation location) {\n            this.id = downloadId++;\n            this.url = url;\n            this.filename = filename;\n            this.mimetype = mimetype;\n            this.isDownloading = false;\n            this.saveToGallery = saveToGallery;\n            this.openOnFinish = open;\n            this.location = location;\n        }\n\n        public int getId() {\n            return id;\n        }\n\n        public boolean isDownloading() {\n            return isDownloading;\n        }\n\n        public void startDownload() {\n            Log.d(TAG, \"startDownload: Starting download\");\n            isDownloading = true;\n            new Thread(() -> {\n                Log.d(TAG, \"startDownload: Thread started\");\n                try {\n                    URL downloadUrl = new URL(url);\n                    connection = (HttpURLConnection) downloadUrl.openConnection();\n                    connection.setInstanceFollowRedirects(true);\n                    connection.setRequestProperty(\"User-Agent\", userAgent);\n                    connection.setConnectTimeout(timeout * 1000);\n                    connection.connect();\n\n                    if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {\n                        GNLog.getInstance().logError(TAG, \"Server returned HTTP \" + connection.getResponseCode()\n                                + \" \" + connection.getResponseMessage());\n                        isDownloading = false;\n                        return;\n                    }\n\n                    String contentDisposition = connection.getHeaderField(\"Content-Disposition\");\n\n                    double fileSizeInMB = connection.getContentLength() / 1048576.0;\n                    Log.d(TAG, \"startDownload: File size in MB: \" + fileSizeInMB);\n\n                    if (connection.getHeaderField(\"Content-Type\") != null)\n                        mimetype = connection.getHeaderField(\"Content-Type\");\n\n                    if (!TextUtils.isEmpty(filename)) {\n                       extension = FileDownloader.getFilenameExtension(filename);\n                       if (TextUtils.isEmpty(extension)) {\n                           extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimetype);\n                       } else if (Objects.equals(filename, extension)) {\n                           filename = \"download\";\n                       } else {\n                           filename = filename.substring(0, filename.length() - (extension.length() + 1));\n                           mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);\n                       }\n                    } else {\n                        // guess file name and extension\n                        String guessedName = LeanUtils.guessFileName(url,\n                                contentDisposition,\n                                mimetype);\n                        int pos = guessedName.lastIndexOf('.');\n\n                        if (pos == -1) {\n                            filename = guessedName;\n                            extension = \"\";\n                        } else if (pos == 0) {\n                            filename = \"download\";\n                            extension = guessedName.substring(1);\n                        } else {\n                            filename = guessedName.substring(0, pos);\n                            extension = guessedName.substring(pos + 1);\n                        }\n\n                        if (!TextUtils.isEmpty(extension)) {\n                            // Update mimetype based on final filename extension\n                            mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);\n                        }\n                    }\n\n                    if (location == FileDownloader.DownloadLocation.PRIVATE_INTERNAL &&\n                            !TextUtils.isEmpty(contentDisposition) &&\n                            contentDisposition.startsWith(\"inline\"))\n                        this.openOnFinish = true;\n\n                    if (location == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {\n                        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {\n                            ContentResolver contentResolver = getApplicationContext().getContentResolver();\n                            if (saveToGallery && mimetype.contains(\"image\")) {\n                                downloadUri = FileDownloader.createExternalFileUri(contentResolver, filename, mimetype, Environment.DIRECTORY_PICTURES);\n                            } else {\n                                downloadUri = FileDownloader.createExternalFileUri(contentResolver, filename, mimetype, Environment.DIRECTORY_DOWNLOADS);\n                                saveToGallery = false;\n                            }\n                            if (downloadUri != null) {\n                                outputStream = (FileOutputStream) contentResolver.openOutputStream(downloadUri);\n                            }\n                        } else {\n                            if (saveToGallery) {\n                                outputFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), filename, extension);\n                            } else {\n                                outputFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), filename, extension);\n                            }\n                            outputStream = new FileOutputStream(outputFile);\n                        }\n                    } else {\n                        this.openOnFinish = true;\n                        outputFile = FileDownloader.createOutputFile(getFilesDir(), filename, extension);\n                        outputStream = new FileOutputStream(outputFile);\n                    }\n\n                    int fileLength = connection.getContentLength();\n                    inputStream = connection.getInputStream();\n\n                    byte[] buffer = new byte[BUFFER_SIZE];\n                    int bytesRead;\n                    int bytesDownloaded = 0;\n\n                    while ((bytesRead = inputStream.read(buffer)) != -1 && isDownloading) {\n                        outputStream.write(buffer, 0, bytesRead);\n                        bytesDownloaded += bytesRead;\n                        int progress = (int) (bytesDownloaded * 100 / fileLength);\n                        Log.d(TAG, \"startDownload: Download progress: \" + progress);\n                    }\n                    if (!isDownloading && outputFile != null) {\n                        outputFile.delete();\n                        outputFile = null;\n                    }\n                } catch (IOException e) {\n                    GNLog.getInstance().logError(TAG, \"startDownload: \", e);\n                } finally {\n                    try {\n                        if (inputStream != null) inputStream.close();\n                        if (outputStream != null) outputStream.close();\n                        if (connection != null) connection.disconnect();\n                    } catch (IOException e) {\n                        GNLog.getInstance().logError(TAG, \"startDownload: \", e);\n                    }\n                    isDownloading = false;\n                    if (downloadUri == null && outputFile != null) {\n                        downloadUri = FileProvider.getUriForFile(DownloadService.this, DownloadService.this.getApplicationContext().getPackageName() + \".fileprovider\", outputFile);\n                    }\n                    handleDownloadUri(location, downloadUri, mimetype, saveToGallery, openOnFinish, filename + \".\" + extension);\n                }\n            }).start();\n        }\n\n        public void cancelDownload() {\n            isDownloading = false;\n            Toast.makeText(DownloadService.this, getString(R.string.download_canceled) + \" \" + filename, Toast.LENGTH_SHORT).show();\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileDownloader.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.ComponentName;\nimport android.content.ContentResolver;\nimport android.content.ContentValues;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.ServiceConnection;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.os.IBinder;\nimport android.provider.MediaStore;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.webkit.DownloadListener;\nimport android.webkit.MimeTypeMap;\nimport android.widget.Toast;\n\nimport androidx.activity.result.ActivityResultLauncher;\nimport androidx.activity.result.contract.ActivityResultContracts;\nimport androidx.core.content.ContextCompat;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Objects;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.LeanUtils;\n\n/**\n * Created by weiyin on 6/24/14.\n */\npublic class FileDownloader implements DownloadListener {\n    public enum DownloadLocation {\n        PUBLIC_DOWNLOADS, PRIVATE_INTERNAL\n    }\n\n    private static final String TAG = FileDownloader.class.getName();\n    private final MainActivity context;\n    private final DownloadLocation defaultDownloadLocation;\n    private final ActivityResultLauncher<String[]> requestPermissionLauncher;\n    private UrlNavigation urlNavigation;\n    private String lastDownloadedUrl;\n    private DownloadService downloadService;\n    private boolean isBound = false;\n    private PreDownloadInfo preDownloadInfo;\n\n    private final ServiceConnection serviceConnection = new ServiceConnection() {\n        @Override\n        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {\n            DownloadService.DownloadBinder binder = (DownloadService.DownloadBinder) iBinder;\n            downloadService = binder.getService();\n            isBound = true;\n        }\n\n        @Override\n        public void onServiceDisconnected(ComponentName componentName) {\n            downloadService = null;\n            isBound = false;\n        }\n    };\n\n    FileDownloader(MainActivity context) {\n        this.context = context;\n\n        AppConfig appConfig = AppConfig.getInstance(this.context);\n        if (appConfig.downloadToPublicStorage) {\n            this.defaultDownloadLocation = DownloadLocation.PUBLIC_DOWNLOADS;\n        } else {\n            this.defaultDownloadLocation = DownloadLocation.PRIVATE_INTERNAL;\n        }\n\n        Intent intent = new Intent(context, DownloadService.class);\n        context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);\n\n        // initialize request permission launcher\n        requestPermissionLauncher = context.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), isGranted -> {\n\n            if (isGranted.containsKey(Manifest.permission.WRITE_EXTERNAL_STORAGE) && Boolean.FALSE.equals(isGranted.get(Manifest.permission.WRITE_EXTERNAL_STORAGE))) {\n                Toast.makeText(context, \"Unable to save download, storage permission denied\", Toast.LENGTH_SHORT).show();\n                return;\n            }\n\n            if (preDownloadInfo != null && isBound) {\n                if (preDownloadInfo.isBlob) {\n                    context.getFileWriterSharer().downloadBlobUrl(preDownloadInfo.url, preDownloadInfo.filename, preDownloadInfo.open);\n                } else {\n                    downloadService.startDownload(preDownloadInfo.url, preDownloadInfo.filename, preDownloadInfo.mimetype, preDownloadInfo.shouldSaveToGallery, preDownloadInfo.open, defaultDownloadLocation);\n                }\n                preDownloadInfo = null;\n            }\n        });\n    }\n\n    @Override\n    public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {\n        if (urlNavigation != null) {\n            urlNavigation.onDownloadStart();\n        }\n\n        if (context != null) {\n            context.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    context.showWebview();\n                }\n            });\n        }\n\n        // get filename from content disposition\n        String guessFilename = null;\n        if (!TextUtils.isEmpty(contentDisposition)) {\n             guessFilename = LeanUtils.guessFileName(url, contentDisposition, mimetype);\n        }\n\n        if (url.startsWith(\"blob:\") && context != null) {\n\n            boolean openAfterDownload = defaultDownloadLocation == DownloadLocation.PRIVATE_INTERNAL;\n\n            if (requestRequiredPermission(new PreDownloadInfo(url, guessFilename, true, openAfterDownload))) {\n                return;\n            }\n\n            context.getFileWriterSharer().downloadBlobUrl(url, guessFilename, openAfterDownload);\n            return;\n        }\n\n        lastDownloadedUrl = url;\n\n        // try to guess mimetype\n        if (mimetype == null || mimetype.equalsIgnoreCase(\"application/force-download\") ||\n                mimetype.equalsIgnoreCase(\"application/octet-stream\")) {\n            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();\n            String extension = MimeTypeMap.getFileExtensionFromUrl(url);\n            if (extension != null && !extension.isEmpty()) {\n                String guessedMimeType = mimeTypeMap.getMimeTypeFromExtension(extension);\n                if (guessedMimeType != null) {\n                    mimetype = guessedMimeType;\n                }\n            }\n        }\n\n        startDownload(url, guessFilename,  mimetype, false, false);\n    }\n\n    public void downloadFile(String url, String filename, boolean shouldSaveToGallery, boolean open) {\n        if (TextUtils.isEmpty(url)) {\n            Log.d(TAG, \"downloadFile: Url empty!\");\n            return;\n        }\n\n        if (url.startsWith(\"blob:\") && context != null) {\n\n            if (defaultDownloadLocation == DownloadLocation.PRIVATE_INTERNAL) {\n                open = true;\n            }\n\n            if (requestRequiredPermission(new PreDownloadInfo(url, filename, true, open))) {\n                return;\n            }\n            context.getFileWriterSharer().downloadBlobUrl(url, filename, open);\n            return;\n        }\n\n        String mimetype = \"*/*\";\n        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();\n        String extension = MimeTypeMap.getFileExtensionFromUrl(url);\n        if (extension != null && !extension.isEmpty()) {\n            String guessedMimeType = mimeTypeMap.getMimeTypeFromExtension(extension);\n            if (guessedMimeType != null) {\n                mimetype = guessedMimeType;\n            }\n        }\n\n        startDownload(url, filename, mimetype, shouldSaveToGallery, open);\n    }\n\n    private void startDownload(String downloadUrl, String filename, String mimetype, boolean shouldSaveToGallery, boolean open) {\n        if (isBound) {\n            if (requestRequiredPermission(new PreDownloadInfo(downloadUrl, filename, mimetype, shouldSaveToGallery, open, false))) return;\n            downloadService.startDownload(downloadUrl, filename, mimetype, shouldSaveToGallery, open, defaultDownloadLocation);\n        }\n    }\n\n    // Requests required permission depending on device's SDK version\n    private boolean requestRequiredPermission(PreDownloadInfo preDownloadInfo) {\n\n        List<String> permissions = new ArrayList<>();\n\n        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P &&\n                ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED &&\n                defaultDownloadLocation == DownloadLocation.PUBLIC_DOWNLOADS) {\n            permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);\n        }\n\n        if (permissions.size() > 0) {\n            this.preDownloadInfo = preDownloadInfo;\n            requestPermissionLauncher.launch(permissions.toArray(new String[] {}));\n            return true;\n        }\n        return false;\n    }\n\n    public String getLastDownloadedUrl() {\n        return lastDownloadedUrl;\n    }\n\n    public void setUrlNavigation(UrlNavigation urlNavigation) {\n        this.urlNavigation = urlNavigation;\n    }\n\n    public void unbindDownloadService() {\n        if (isBound) {\n            context.unbindService(serviceConnection);\n            isBound = false;\n        }\n    }\n\n    public static Uri createExternalFileUri(ContentResolver contentResolver, String filename, String mimetype, String environment) {\n        ContentValues contentValues = new ContentValues();\n        contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, filename);\n        contentValues.put(MediaStore.MediaColumns.MIME_TYPE, mimetype);\n\n        if (Objects.equals(environment, Environment.DIRECTORY_PICTURES)) {\n            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES);\n            return contentResolver.insert(MediaStore.Images.Media.getContentUri(\"external\"), contentValues);\n        } else if (Objects.equals(environment, Environment.DIRECTORY_DOWNLOADS)) {\n            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);\n            return contentResolver.insert(MediaStore.Files.getContentUri(\"external\"), contentValues);\n        }\n        return null;\n    }\n\n    public static File createOutputFile(File dir, String filename, String extension) {\n        return new File(dir, FileDownloader.getUniqueFileName(filename + \".\" + extension, dir));\n    }\n\n    public static String getUniqueFileName(String fileName, File dir) {\n        File file = new File(dir, fileName);\n\n        if (!file.exists()) {\n            return fileName;\n        }\n\n        int count = 1;\n        String nameWithoutExt = fileName.substring(0, fileName.lastIndexOf('.'));\n        String ext = fileName.substring(fileName.lastIndexOf('.'));\n        String newFileName = nameWithoutExt + \"_\" + count + ext;\n        file = new File(dir, newFileName);\n\n        while (file.exists()) {\n            count++;\n            newFileName = nameWithoutExt + \"_\" + count + ext;\n            file = new File(dir, newFileName);\n        }\n\n        return file.getName();\n    }\n\n    public static String getFilenameExtension(String name) {\n        int pos = name.lastIndexOf('.');\n        if (pos == -1) {\n            return null;\n        } else if (pos == 0) {\n            return name;\n        } else {\n            return name.substring(pos + 1);\n        }\n    }\n\n    private static class PreDownloadInfo {\n        String url;\n        String filename;\n        String mimetype;\n        boolean shouldSaveToGallery;\n        boolean open;\n        boolean isBlob;\n\n        public PreDownloadInfo(String url, String filename, String mimetype, boolean shouldSaveToGallery, boolean open, boolean isBlob) {\n            this.url = url;\n            this.filename = filename;\n            this.mimetype = mimetype;\n            this.shouldSaveToGallery = shouldSaveToGallery;\n            this.open = open;\n            this.isBlob = isBlob;\n        }\n\n        public PreDownloadInfo(String url, String filename, boolean isBlob, boolean open) {\n            this.url = url;\n            this.filename = filename;\n            this.isBlob = isBlob;\n            this.open = open;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileUploadIntentsCreator.kt",
    "content": "package io.gonative.android\n\nimport android.annotation.SuppressLint\nimport android.app.Activity\nimport android.content.*\nimport android.content.pm.PackageManager\nimport android.content.pm.ResolveInfo\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.os.Parcelable\nimport android.provider.MediaStore\nimport android.webkit.MimeTypeMap\nimport androidx.core.content.FileProvider\nimport io.gonative.gonative_core.AppConfig\nimport io.gonative.gonative_core.Utils\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.*\n\n\nclass FileUploadIntentsCreator(val context: Context, val mimeTypeSpecs: Array<String>, val multiple: Boolean) {\n    private val mimeTypes = hashSetOf<String>()\n    private val appConfig = AppConfig.getInstance(context)\n    private var packageManger = context.packageManager\n\n    var currentCaptureUri: Uri? = null\n\n    init {\n        extractMimeTypes()\n    }\n\n    private fun extractMimeTypes() {\n        mimeTypeSpecs.forEach { spec ->\n            val specParts = spec.split(\"[,;\\\\s]\")\n            specParts.forEach {\n                if (it.startsWith(\".\")) {\n                    val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.substring(1))\n                    mimeType?.let { it1 -> mimeTypes.add(it1) }\n                } else if (it.contains(\"/\")) {\n                    mimeTypes.add(it)\n                }\n            }\n        }\n\n        if (mimeTypes.isEmpty()) {\n            mimeTypes.add(\"*/*\")\n        }\n    }\n\n    fun imagesAllowed(): Boolean {\n        if (!Utils.isPermissionGranted(context as Activity, android.Manifest.permission.CAMERA)) return false\n        return mimeTypes.contains(\"*/*\") || mimeTypes.any { it.contains(\"image/\") }\n    }\n\n    fun videosAllowed(): Boolean {\n        if (!Utils.isPermissionGranted(context as Activity, android.Manifest.permission.CAMERA)) return false\n        return mimeTypes.contains(\"*/*\") || mimeTypes.any { it.contains(\"video/\") }\n    }\n\n    private fun photoCameraIntents(): ArrayList<Intent> {\n        val intents = arrayListOf<Intent>()\n\n        if (!appConfig.directCameraUploads) {\n            return intents\n        }\n\n        val timeStamp = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.US).format(Date())\n        val imageFileName = \"IMG_$timeStamp.jpg\"\n        val storageDir = this.context.filesDir\n        val captureFile = File(storageDir, imageFileName)\n\n        currentCaptureUri = FileProvider.getUriForFile(context, context.applicationContext.packageName + \".fileprovider\", captureFile);\n\n        val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)\n        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)\n        for (resolve in resolveList) {\n            val packageName = resolve.activityInfo.packageName\n            val intent = Intent(captureIntent)\n            intent.component = ComponentName(resolve.activityInfo.packageName, resolve.activityInfo.name)\n            intent.setPackage(packageName)\n            intent.putExtra(MediaStore.EXTRA_OUTPUT, currentCaptureUri)\n            intents.add(intent)\n        }\n\n        return intents\n    }\n\n    private fun videoCameraIntents(): ArrayList<Intent> {\n        val intents = arrayListOf<Intent>()\n\n        if (!appConfig.directCameraUploads) {\n            return intents\n        }\n\n        val captureIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE)\n        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)\n        for (resolve in resolveList) {\n            val packageName = resolve.activityInfo.packageName\n            val intent = Intent(captureIntent)\n            intent.component = ComponentName(resolve.activityInfo.packageName, resolve.activityInfo.name)\n            intent.setPackage(packageName)\n            intents.add(intent)\n        }\n\n        return intents\n    }\n\n    private fun filePickerIntent(): Intent {\n        var intent: Intent\n        intent = Intent(Intent.ACTION_GET_CONTENT) // or ACTION_OPEN_DOCUMENT\n        intent.type = mimeTypes.joinToString(\", \")\n        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes.toTypedArray())\n        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)\n        intent.addCategory(Intent.CATEGORY_OPENABLE)\n\n        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(intent)\n\n        if (resolveList.isEmpty() && Build.MANUFACTURER.equals(\"samsung\", ignoreCase = true)) {\n            intent = Intent(\"com.sec.android.app.myfiles.PICK_DATA\")\n            intent.putExtra(\"CONTENT_TYPE\", \"*/*\")\n            intent.addCategory(Intent.CATEGORY_DEFAULT)\n            return intent\n        }\n\n        return intent\n    }\n\n    fun cameraIntent(): Intent {\n        val mediaIntents = if (imagesAllowed()) {\n            photoCameraIntents()\n        } else {\n            videoCameraIntents()\n        }\n        return mediaIntents.first()\n    }\n\n    @SuppressLint(\"IntentReset\")\n    fun chooserIntent(): Intent {\n        val directCaptureIntents = arrayListOf<Intent>()\n        if (imagesAllowed()) {\n            directCaptureIntents.addAll(photoCameraIntents())\n        }\n        if (videosAllowed()) {\n            directCaptureIntents.addAll(videoCameraIntents())\n        }\n\n        val chooserIntent: Intent?\n        val mediaIntent: Intent?\n\n        if (imagesAllowed() xor videosAllowed()) {\n            mediaIntent = getMediaInitialIntent()\n            mediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)\n            chooserIntent = Intent.createChooser(mediaIntent, context.getString(R.string.choose_action))\n        } else if (onlyImagesAndVideo() && !isGooglePhotosDefaultApp()) {\n            mediaIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)\n            mediaIntent.type = \"image/*, video/*\"\n            mediaIntent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf(\"image/*\", \"video/*\"))\n            mediaIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiple)\n            chooserIntent = Intent.createChooser(mediaIntent, context.getString(R.string.choose_action))\n        } else {\n            chooserIntent = Intent.createChooser(filePickerIntent(), context.getString(R.string.choose_action))\n        }\n        chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, directCaptureIntents.toTypedArray<Parcelable>())\n\n        return chooserIntent\n    }\n\n    private fun getMediaInitialIntent(): Intent {\n        return if (imagesAllowed()) {\n            Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)\n        } else {\n            Intent(Intent.ACTION_PICK, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)\n        }\n    }\n\n    private fun onlyImagesAndVideo(): Boolean {\n        return mimeTypes.all { it.startsWith(\"image/\") || it.startsWith(\"video/\") }\n    }\n\n    private fun isGooglePhotosDefaultApp(): Boolean {\n        val captureIntent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)\n        val resolveList: List<ResolveInfo> = listOfAvailableAppsForIntent(captureIntent)\n\n        return resolveList.size == 1 && resolveList.first().activityInfo.packageName == \"com.google.android.apps.photos\"\n    }\n\n    private fun listOfAvailableAppsForIntent(intent: Intent): List<ResolveInfo> {\n        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n            packageManger.queryIntentActivities(intent, PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY.toLong()))\n        } else {\n            @Suppress(\"DEPRECATION\")\n            packageManger.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)\n        }\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/FileWriterSharer.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.ActivityNotFoundException;\nimport android.content.ContentResolver;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Build;\nimport android.os.Environment;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Log;\nimport android.webkit.JavascriptInterface;\nimport android.webkit.MimeTypeMap;\nimport android.widget.Toast;\n\nimport androidx.core.content.FileProvider;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.BufferedInputStream;\nimport java.io.BufferedOutputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.Objects;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.LeanUtils;\n\npublic class FileWriterSharer {\n    private static final String TAG = FileWriterSharer.class.getSimpleName();\n    private static final long MAX_SIZE = 1024 * 1024 * 1024; // 1 gigabyte\n    private static final String BASE64TAG = \";base64,\";\n    private final FileDownloader.DownloadLocation defaultDownloadLocation;\n    private String downloadFilename;\n    private boolean open = false;\n\n    private static class FileInfo{\n        public String id;\n        public String name;\n        public long size;\n        public String mimetype;\n        public String extension;\n        public File savedFile;\n        public Uri savedUri;\n        public OutputStream fileOutputStream;\n        public long bytesWritten;\n    }\n\n    private class JavascriptBridge {\n        @JavascriptInterface\n        public void postMessage(String jsonMessage) {\n            Log.d(TAG, \"got message \" + jsonMessage);\n            try {\n                JSONObject json = new JSONObject(jsonMessage);\n                String event = LeanUtils.optString(json, \"event\");\n                if (\"fileStart\".equals(event)) {\n                    onFileStart(json);\n                } else if (\"fileChunk\".equals(event)) {\n                    onFileChunk(json);\n                } else if (\"fileEnd\".equals(event)) {\n                    onFileEnd(json);\n                } else if (\"nextFileInfo\".equals(event)) {\n                    onNextFileInfo(json);\n                } else {\n                    GNLog.getInstance().logError(TAG, \"Invalid event \" + event);\n                }\n            } catch (JSONException e) {\n                GNLog.getInstance().logError(TAG, \"Error parsing message as json\", e);\n            } catch (IOException e) {\n                GNLog.getInstance().logError(TAG, \"IO Error\", e);\n            }\n        }\n    }\n\n    private JavascriptBridge javascriptBridge;\n    private MainActivity context;\n    private Map<String, FileInfo> idToFileInfo;\n    private String nextFileName;\n\n    public FileWriterSharer(MainActivity context) {\n        this.javascriptBridge = new JavascriptBridge();\n        this.context = context;\n        this.idToFileInfo = new HashMap<>();\n\n        AppConfig appConfig = AppConfig.getInstance(this.context);\n        if (appConfig.downloadToPublicStorage) {\n            this.defaultDownloadLocation = FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS;\n        } else {\n            this.defaultDownloadLocation = FileDownloader.DownloadLocation.PRIVATE_INTERNAL;\n        }\n    }\n\n    public JavascriptBridge getJavascriptBridge() {\n        return javascriptBridge;\n    }\n\n    public void downloadBlobUrl(String url, String filename, boolean open) {\n        if (url == null || !url.startsWith(\"blob:\")) {\n            return;\n        }\n\n        this.downloadFilename = filename;\n        this.open = open;\n\n        try {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            BufferedInputStream is = new BufferedInputStream(context.getAssets().open(\"BlobDownloader.js\"));\n            IOUtils.copy(is, baos);\n            String js = baos.toString();\n            context.runJavascript(js);\n            js = \"gonativeDownloadBlobUrl(\" + LeanUtils.jsWrapString(url) + \")\";\n            context.runJavascript(js);\n        } catch (IOException e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n    }\n\n    private void onFileStart(JSONObject message) throws IOException {\n        String identifier = LeanUtils.optString(message, \"id\");\n        if (identifier == null || identifier.isEmpty()) {\n            GNLog.getInstance().logError(TAG, \"Invalid file id\");\n            return;\n        }\n\n        String fileName;\n        String extension = null;\n        String type = null;\n\n        if (!TextUtils.isEmpty(downloadFilename)) {\n            extension = FileDownloader.getFilenameExtension(downloadFilename);\n            if (!TextUtils.isEmpty(extension)) {\n                if (Objects.equals(extension, downloadFilename)) {\n                    fileName = \"download\";\n                } else {\n                    fileName = downloadFilename.substring(0, downloadFilename.length() - (extension.length() + 1));\n                }\n                type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);\n            } else {\n                fileName = downloadFilename;\n            }\n        } else {\n            fileName = LeanUtils.optString(message, \"name\");\n            if (fileName == null || fileName.isEmpty()) {\n                if (this.nextFileName != null) {\n                    fileName = this.nextFileName;\n                    this.nextFileName = null;\n                } else {\n                    fileName = \"download\";\n                }\n            }\n        }\n\n        long fileSize = message.optLong(\"size\", -1);\n        if (fileSize <= 0 || fileSize > MAX_SIZE) {\n            GNLog.getInstance().logError(TAG, \"Invalid file size\");\n            return;\n        }\n\n        if (TextUtils.isEmpty(type)) {\n            type = LeanUtils.optString(message, \"type\");\n            if (TextUtils.isEmpty(type)) {\n                GNLog.getInstance().logError(TAG, \"Invalid file type\");\n                return;\n            }\n        }\n\n        if (TextUtils.isEmpty(extension)) {\n            MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();\n            extension = mimeTypeMap.getExtensionFromMimeType(type);\n        }\n\n        final FileInfo info = new FileInfo();\n        info.id = identifier;\n        info.name = fileName;\n        info.size = fileSize;\n        info.mimetype = type;\n        info.extension = extension;\n\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && defaultDownloadLocation == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {\n            // request permissions\n            context.getPermission(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, (permissions, grantResults) -> {\n                try {\n                    onFileStartAfterPermission(info, grantResults[0] == PackageManager.PERMISSION_GRANTED);\n                    final String js = \"gonativeGotStoragePermissions()\";\n                    context.runOnUiThread(() -> context.runJavascript(js));\n                } catch (IOException e) {\n                    GNLog.getInstance().logError(TAG, \"IO Error\", e);\n                }\n            });\n        } else {\n            onFileStartAfterPermission(info, true);\n            final String js = \"gonativeGotStoragePermissions()\";\n            context.runOnUiThread(() -> context.runJavascript(js));\n        }\n    }\n\n    private void onFileStartAfterPermission(FileInfo info, boolean granted) throws IOException {\n        if (granted && defaultDownloadLocation == FileDownloader.DownloadLocation.PUBLIC_DOWNLOADS) {\n            if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {\n                ContentResolver contentResolver = context.getApplicationContext().getContentResolver();\n                Uri uri = FileDownloader.createExternalFileUri(contentResolver, info.name, info.mimetype, Environment.DIRECTORY_DOWNLOADS);\n                if (uri != null) {\n                    info.fileOutputStream = contentResolver.openOutputStream(uri);\n                    info.savedUri = uri;\n                }\n            } else {\n                info.savedFile = FileDownloader.createOutputFile(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), info.name, info.extension);\n                info.fileOutputStream = new BufferedOutputStream(new FileOutputStream(info.savedFile));\n            }\n        } else {\n            info.savedFile = FileDownloader.createOutputFile(context.getFilesDir(), info.name, info.extension);\n            info.fileOutputStream = new BufferedOutputStream(new FileOutputStream(info.savedFile));\n        }\n        info.bytesWritten = 0;\n        this.idToFileInfo.put(info.id, info);\n    }\n\n    private void onFileChunk(JSONObject message) throws IOException {\n        String identifier = LeanUtils.optString(message, \"id\");\n        if (identifier == null || identifier.isEmpty()) {\n            return;\n        }\n\n        FileInfo fileInfo = this.idToFileInfo.get(identifier);\n        if (fileInfo == null) {\n            return;\n        }\n\n        String data = LeanUtils.optString(message, \"data\");\n        if (data == null) {\n            return;\n        }\n\n        int idx = data.indexOf(BASE64TAG);\n        if (idx == -1) {\n            return;\n        }\n\n        idx += BASE64TAG.length();\n        byte[] chunk = Base64.decode(data.substring(idx), Base64.DEFAULT);\n\n        if (fileInfo.bytesWritten + chunk.length > fileInfo.size) {\n            GNLog.getInstance().logError(TAG, \"Received too many bytes. Expected \" + fileInfo.size);\n            try {\n                fileInfo.fileOutputStream.close();\n                fileInfo.savedFile.delete();\n                this.idToFileInfo.remove(identifier);\n            } catch (Exception ignored) {\n\n            }\n\n            return;\n        }\n\n        fileInfo.fileOutputStream.write(chunk);\n        fileInfo.bytesWritten += chunk.length;\n    }\n\n    private void onFileEnd(JSONObject message) throws IOException {\n        String identifier = LeanUtils.optString(message, \"id\");\n        if (identifier == null || identifier.isEmpty()) {\n            GNLog.getInstance().logError(TAG, \"Invalid identifier \" + identifier + \" for fileEnd\");\n            return;\n        }\n\n        final FileInfo fileInfo = this.idToFileInfo.get(identifier);\n        if (fileInfo == null) {\n            GNLog.getInstance().logError(TAG, \"Invalid identifier \" + identifier + \" for fileEnd\");\n            return;\n        }\n\n        fileInfo.fileOutputStream.close();\n\n        if (open) {\n            context.runOnUiThread(() -> {\n                if (fileInfo.savedUri == null && fileInfo.savedFile != null) {\n                    fileInfo.savedUri = FileProvider.getUriForFile(context, context.getApplicationContext().getPackageName() + \".fileprovider\", fileInfo.savedFile);\n                }\n                if (fileInfo.savedUri == null) return;\n\n                Intent intent = getIntentToOpenFile(fileInfo.savedUri, fileInfo.mimetype);\n                try {\n                    context.startActivity(intent);\n                } catch (ActivityNotFoundException e) {\n                    String message1 = context.getResources().getString(R.string.file_handler_not_found);\n                    Toast.makeText(context, message1, Toast.LENGTH_LONG).show();\n                }\n            });\n        } else {\n            String downloadCompleteMessage = fileInfo.name != null && !fileInfo.name.isEmpty()\n                    ? String.format(context.getString(R.string.file_download_finished_with_name), fileInfo.name + '.' + fileInfo.extension)\n                    : context.getString(R.string.file_download_finished);\n            Toast.makeText(context, downloadCompleteMessage, Toast.LENGTH_SHORT).show();\n        }\n    }\n\n    private Intent getIntentToOpenFile(Uri uri, String mimetype) {\n        Intent intent = new Intent(Intent.ACTION_VIEW);\n        intent.setDataAndType(uri, mimetype);\n        intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_TASK);\n        return intent;\n    }\n\n    private void onNextFileInfo(JSONObject message) {\n        String name = LeanUtils.optString(message, \"name\");\n        if (name == null || name.isEmpty()) {\n            GNLog.getInstance().logError(TAG, \"Invalid name for nextFileInfo\");\n            return;\n        }\n        this.nextFileName = name;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/GoNativeApplication.java",
    "content": "package io.gonative.android;\n\nimport android.os.Message;\nimport android.webkit.ValueCallback;\nimport android.widget.Toast;\n\nimport androidx.appcompat.app.AppCompatDelegate;\nimport androidx.multidex.MultiDexApplication;\n\nimport java.util.List;\nimport java.util.Map;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.Bridge;\nimport io.gonative.gonative_core.BridgeModule;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 9/2/15.\n * Copyright 2014 GoNative.io LLC\n */\npublic class GoNativeApplication extends MultiDexApplication {\n\n    private LoginManager loginManager;\n    private RegistrationManager registrationManager;\n    private WebViewPool webViewPool;\n    private Message webviewMessage;\n    private ValueCallback webviewValueCallback;\n    private GoNativeWindowManager goNativeWindowManager;\n    private List<BridgeModule> plugins;\n    private final static String TAG = GoNativeApplication.class.getSimpleName();\n    public final Bridge mBridge = new Bridge(this) {\n        @Override\n        protected List<BridgeModule> getPlugins() {\n            if (GoNativeApplication.this.plugins == null) {\n                GoNativeApplication.this.plugins = new PackageList(GoNativeApplication.this).getPackages();\n            }\n\n            return  GoNativeApplication.this.plugins;\n        }\n    };\n\n    @Override\n    public void onCreate() {\n        super.onCreate();\n        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);\n\n        mBridge.onApplicationCreate(this);\n\n        AppConfig appConfig = AppConfig.getInstance(this);\n        if (appConfig.configError != null) {\n            Toast.makeText(this, \"Invalid appConfig json\", Toast.LENGTH_LONG).show();\n            GNLog.getInstance().logError(TAG, \"AppConfig error\", appConfig.configError);\n        }\n\n        this.loginManager = new LoginManager(this);\n\n        if (appConfig.registrationEndpoints != null) {\n            this.registrationManager = new RegistrationManager(this);\n            registrationManager.processConfig(appConfig.registrationEndpoints);\n        }\n\n        // some global webview setup\n        WebViewSetup.setupWebviewGlobals(this);\n\n        webViewPool = new WebViewPool();\n\n        goNativeWindowManager = new GoNativeWindowManager();\n    }\n\n    public LoginManager getLoginManager() {\n        return loginManager;\n    }\n\n    public RegistrationManager getRegistrationManager() {\n        return registrationManager;\n    }\n\n    public WebViewPool getWebViewPool() {\n        return webViewPool;\n    }\n\n    public Message getWebviewMessage() {\n        return webviewMessage;\n    }\n\n    public void setWebviewMessage(Message webviewMessage) {\n        this.webviewMessage = webviewMessage;\n    }\n\n    public Map<String, Object> getAnalyticsProviderInfo() {\n        return mBridge.getAnalyticsProviderInfo();\n    }\n\n    // Needed for Crosswalk\n    @SuppressWarnings(\"unused\")\n    public ValueCallback getWebviewValueCallback() {\n        return webviewValueCallback;\n    }\n\n    public void setWebviewValueCallback(ValueCallback webviewValueCallback) {\n        this.webviewValueCallback = webviewValueCallback;\n    }\n\n    public GoNativeWindowManager getWindowManager() {\n        return goNativeWindowManager;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/GoNativeWindowManager.java",
    "content": "package io.gonative.android;\n\nimport android.text.TextUtils;\n\nimport java.util.LinkedHashMap;\nimport java.util.Map;\n\npublic class GoNativeWindowManager {\n    private final LinkedHashMap<String, ActivityWindow> windows;\n    private ExcessWindowsClosedListener excessWindowsClosedListener;\n\n    public GoNativeWindowManager() {\n        windows = new LinkedHashMap<>();\n    }\n\n    public void addNewWindow(String activityId, boolean isRoot) {\n        this.windows.put(activityId, new ActivityWindow(activityId, isRoot));\n    }\n\n    public void removeWindow(String activityId) {\n        this.windows.remove(activityId);\n\n        if (excessWindowsClosedListener != null && windows.size() <= 1) {\n            excessWindowsClosedListener.onAllExcessWindowClosed();\n        }\n    }\n\n    public void setOnExcessWindowClosedListener(ExcessWindowsClosedListener listener) {\n        this.excessWindowsClosedListener = listener;\n    }\n\n    public ActivityWindow getActivityWindowInfo(String activityId) {\n        return windows.get(activityId);\n    }\n\n    public void setUrlLevel(String activityId, int urlLevel) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            window.setUrlLevels(urlLevel, window.parentUrlLevel);\n        }\n    }\n\n    public int getUrlLevel(String activityId) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            return window.urlLevel;\n        }\n        return -1;\n    }\n\n    public void setParentUrlLevel(String activityId, int parentLevel) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            window.setUrlLevels(window.urlLevel, parentLevel);\n        }\n    }\n\n    public int getParentUrlLevel(String activityId) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            return window.parentUrlLevel;\n        }\n        return -1;\n    }\n\n    public void setUrlLevels(String activityId, int urlLevel, int parentLevel) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            window.setUrlLevels(urlLevel, parentLevel);\n        }\n    }\n\n    public boolean isRoot(String activityId) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            return window.isRoot;\n        }\n        return false;\n    }\n\n    public void setAsNewRoot(String activityId) {\n        for (Map.Entry<String, ActivityWindow> entry : windows.entrySet()) {\n            ActivityWindow window = entry.getValue();\n            if (TextUtils.equals(activityId, entry.getKey())) {\n                window.isRoot = true;\n            } else {\n                window.isRoot = false;\n            }\n        }\n    }\n\n    public void setIgnoreInterceptMaxWindows(String activityId, boolean ignore) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            window.ignoreInterceptMaxWindows = ignore;\n        }\n    }\n\n    public boolean isIgnoreInterceptMaxWindows(String activityId) {\n        ActivityWindow window = windows.get(activityId);\n        if (window != null) {\n            return window.ignoreInterceptMaxWindows;\n        }\n        return false;\n    }\n\n    public int getWindowCount() {\n        return windows.size();\n    }\n\n    // Returns ID of the next window after root as Excess window\n    public String getExcessWindow() {\n        for (Map.Entry<String, ActivityWindow> entry : windows.entrySet()) {\n            ActivityWindow window = entry.getValue();\n            if (window.isRoot) continue;\n            return window.id;\n        }\n        return null;\n    }\n\n    public static class ActivityWindow {\n        private final String id;\n        private boolean isRoot;\n        private int urlLevel;\n        private int parentUrlLevel;\n        private boolean ignoreInterceptMaxWindows;\n\n        ActivityWindow(String id, boolean isRoot) {\n            this.id = id;\n            this.isRoot = isRoot;\n            this.urlLevel = -1;\n            this.parentUrlLevel = -1;\n        }\n\n        public void setUrlLevels(int urlLevel, int parentUrlLevel) {\n            this.urlLevel = urlLevel;\n            this.parentUrlLevel = parentUrlLevel;\n        }\n\n        @Override\n        public String toString() {\n            return \"id=\" + id + \"\\n\" +\n                    \"isRoot=\" + isRoot + \"\\n\" +\n                    \"urlLevel=\" + urlLevel + \"\\n\" +\n                    \"parentUrlLevel=\" + parentUrlLevel;\n        }\n    }\n\n\n    interface ExcessWindowsClosedListener {\n        void onAllExcessWindowClosed();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/HtmlIntercept.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\n\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.net.HttpURLConnection;\nimport java.net.MalformedURLException;\nimport java.net.URL;\nimport java.util.Locale;\nimport java.util.Map;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Created by weiyin on 1/29/16.\n */\n\npublic class HtmlIntercept {\n    private static final String TAG = HtmlIntercept.class.getName();\n\n    private Context context;\n    private String interceptUrl;\n    private String JSBridgeScript;\n    private String redirectedUrl;\n\n    // track whether we have intercepted a page at all. We will always try to intercept the first time,\n    // because interceptUrl may not have been set if restoring from a bundle.\n    private boolean hasIntercepted = false;\n\n    HtmlIntercept(Context context) {\n        this.context = context;\n    }\n\n    public void setInterceptUrl(String interceptUrl) {\n        this.interceptUrl = interceptUrl;\n    }\n\n    public WebResourceResponse interceptHtml(GoNativeWebviewInterface view, String url, String referer) {\n\n        AppConfig appConfig = AppConfig.getInstance(context);\n        if (!appConfig.interceptHtml && (appConfig.customHeaders == null || appConfig.customHeaders.isEmpty())) return null;\n\n        if (!hasIntercepted) {\n            interceptUrl = url;\n            hasIntercepted = true;\n        }\n        if (!urlMatches(interceptUrl, url)) return null;\n\n        InputStream is = null;\n        ByteArrayOutputStream baos = null;\n\n        try {\n            URL parsedUrl = new URL(url);\n            String protocol = parsedUrl.getProtocol();\n            if (!protocol.equalsIgnoreCase(\"http\") && !protocol.equalsIgnoreCase(\"https\")) return null;\n\n            HttpURLConnection connection = (HttpURLConnection)parsedUrl.openConnection();\n            connection.setInstanceFollowRedirects(false);\n            String customUserAgent = appConfig.userAgentForUrl(parsedUrl.toString());\n            if (customUserAgent != null) {\n                connection.setRequestProperty(\"User-Agent\", customUserAgent);\n            } else {\n                connection.setRequestProperty(\"User-Agent\", appConfig.userAgent);\n            }\n            connection.setRequestProperty(\"Cache-Control\", \"no-cache\");\n\n            if (referer != null) {\n                connection.setRequestProperty(\"Referer\", referer);\n            }\n\n            Map<String, String> customHeaders = CustomHeaders.getCustomHeaders(context);\n            if (customHeaders != null) {\n                for (Map.Entry<String, String> entry : customHeaders.entrySet()) {\n                    connection.setRequestProperty(entry.getKey(), entry.getValue());\n                }\n            }\n\n            connection.connect();\n            int responseCode = connection.getResponseCode();\n\n            if (responseCode == HttpURLConnection.HTTP_MOVED_PERM ||\n                    responseCode == HttpURLConnection.HTTP_MOVED_TEMP ||\n                    responseCode == HttpURLConnection.HTTP_SEE_OTHER ||\n                    responseCode == 307) {\n                // Get redirect URL to be loaded directly to webview, return blank resource which we cancel on UrlNavigation.onPageStart()\n                // We cannot pass headers in webresourceresponse until Android API 21, and we cannot return null\n                // or else the webview will handle the request entirely without intercept\n                String location = connection.getHeaderField(\"Location\");\n\n                // validate location as URL\n                try {\n                    new URL(location);\n                } catch (MalformedURLException ex) {\n                    URL base = new URL(url);\n                    location = new URL(base, location).toString();\n                }\n\n                if (!TextUtils.isEmpty(location)) {\n                    this.redirectedUrl = url;\n                    MainActivity mainActivity = (MainActivity) context;\n                    WebView webView = (WebView) mainActivity.getWebView();\n                    String finalLocation = location; // needed, for this should be effectively final\n                    webView.post(() -> webView.loadUrl(finalLocation));\n                }\n                return new WebResourceResponse(\"text/html\", \"utf-8\", new ByteArrayInputStream(\"\".getBytes()));\n            }\n\n            String mimetype = connection.getContentType();\n            if (mimetype == null) {\n                try {\n                    is = new BufferedInputStream(connection.getInputStream());\n                } catch (IOException e) {\n                    is = new BufferedInputStream(connection.getErrorStream());\n                }\n                mimetype = HttpURLConnection.guessContentTypeFromStream(is);\n            }\n\n            // if not html, then return null so that webview loads directly.\n            if (mimetype == null || !mimetype.startsWith(\"text/html\"))\n                return null;\n\n            // get and intercept the data\n            String characterEncoding = getCharset(mimetype);\n            if (characterEncoding == null) {\n                characterEncoding = \"UTF-8\";\n            } else if (characterEncoding.toLowerCase().equals(\"iso-8859-1\")) {\n                // windows-1252 is a superset of ios-8859-1 that supports the euro symbol €.\n                // The html5 spec actually maps \"iso-8859-1\" to windows-1252 encoding\n                characterEncoding = \"windows-1252\";\n            }\n\n            if (is == null) {\n                try {\n                    is = new BufferedInputStream(connection.getInputStream());\n                } catch (IOException e) {\n                    is = new BufferedInputStream(connection.getErrorStream());\n                }\n            }\n\n            int initialLength = connection.getContentLength();\n            if (initialLength < 0)\n                initialLength = UrlNavigation.DEFAULT_HTML_SIZE;\n\n            baos = new ByteArrayOutputStream(initialLength);\n            IOUtils.copy(is, baos);\n            String origString;\n            try {\n                origString = baos.toString(characterEncoding);\n            } catch (UnsupportedEncodingException e){\n                // Everything should support UTF-8\n                origString = baos.toString(\"UTF-8\");\n            }\n\n            // modify the string!\n            String newString;\n            int insertPoint = origString.indexOf(\"</head>\");\n            if (insertPoint >= 0) {\n                StringBuilder builder = new StringBuilder(initialLength);\n                builder.append(origString.substring(0, insertPoint));\n                if (appConfig.customCSS != null || appConfig.androidCustomCSS != null) {\n                    builder.append(\"<style>\");\n                    if(appConfig.customCSS != null)\n                        builder.append(appConfig.customCSS).append(\" \");\n                    if(appConfig.androidCustomCSS != null)\n                        builder.append(appConfig.androidCustomCSS);\n                    builder.append(\"</style>\");\n                }\n\n                if (appConfig.customJS != null || appConfig.androidCustomJS != null) {\n                    builder.append(\"<script>\");\n                    if(appConfig.customJS != null)\n                        builder.append(appConfig.customJS).append(\" \");\n                    if(appConfig.androidCustomJS != null)\n                        builder.append(appConfig.androidCustomJS);\n                    builder.append(\"</script>\");\n                }\n\n                if (appConfig.stringViewport != null) {\n                    builder.append(\"<meta name=\\\"viewport\\\" content=\\\"\");\n                    builder.append(TextUtils.htmlEncode(appConfig.stringViewport));\n                    builder.append(\"\\\" />\");\n                }\n                if (!Double.isNaN(appConfig.forceViewportWidth)) {\n                    if (appConfig.zoomableForceViewport) {\n                        builder.append(String.format(Locale.US, \"<meta name=\\\"viewport\\\" content=\\\"width=%f,maximum-scale=1.0\\\" />\",\n                                appConfig.forceViewportWidth));\n                    }\n                    else {\n                        // we want to use user-scalable=no, but android has a bug that sets scale to\n                        // 1.0 if user-scalable=no. The workaround to is calculate the scale and set\n                        // it for initial, minimum, and maximum.\n                        // http://stackoverflow.com/questions/12723844/android-viewport-setting-user-scalable-no-breaks-width-zoom-level-of-viewpor\n                        double webViewWidth = view.getWidth() / context.getResources().getDisplayMetrics().density;\n                        double viewportWidth = appConfig.forceViewportWidth;\n                        double scale = webViewWidth / viewportWidth;\n                        builder.append(String.format(Locale.US, \"<meta name=\\\"viewport\\\" content=\\\"width=%f,initial-scale=%f,minimum-scale=%f,maximum-scale=%f\\\" />\",\n                                viewportWidth, scale, scale, scale));\n                    }\n                }\n\n                builder.append(origString.substring(insertPoint));\n                newString = builder.toString();\n            }\n            else {\n                Log.d(TAG, \"could not find closing </head> tag\");\n                newString = origString;\n            }\n\n            return new WebResourceResponse(\"text/html\", \"UTF-8\",\n                    new ByteArrayInputStream(newString.getBytes(\"UTF-8\")));\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.toString(), e);\n            return null;\n        } finally {\n            IOUtils.close(is);\n            IOUtils.close(baos);\n        }\n    }\n\n    // Do these urls match, ignoring trailing slash in path\n    private static boolean urlMatches(String url1, String url2) {\n        if (url1 == null || url2 == null) return false;\n\n        try {\n            URL parsed1 = new URL(url1);\n            URL parsed2 = new URL(url2);\n\n            if (stringsNotEqual(parsed1.getProtocol(), parsed2.getProtocol())) return false;\n\n            if (stringsNotEqual(parsed1.getAuthority(), parsed2.getAuthority())) return false;\n\n            if (stringsNotEqual(parsed1.getQuery(), parsed2.getQuery())) return false;\n\n            String path1 = parsed1.getPath();\n            String path2 = parsed2.getPath();\n            if (path1 == null) path1 = \"\";\n            if (path2 == null) path2 = \"\";\n\n            int lengthDiff = path2.length() - path2.length();\n            if (lengthDiff > 1 || lengthDiff < -1) return false;\n            if (lengthDiff == 0) return path1.equals(path2);\n            if (lengthDiff == 1) {\n                return path2.equals(path1 + \"/\");\n            }\n\n            // lengthDiff == -1\n            return path1.equals(path2 + \"/\");\n        } catch (MalformedURLException e) {\n            return false;\n        }\n    }\n\n    private static boolean stringsNotEqual(String s1, String s2) {\n        return !(s1 == null ? s2 == null : s1.equals(s2));\n    }\n\n    private static String getCharset(String contentType) {\n        if (contentType == null || contentType.isEmpty()) {\n            return null;\n        }\n\n        String[] tokens = contentType.split(\"; *\");\n        for (String s : tokens) {\n            if (s.startsWith(\"charset=\")) {\n                return s.substring(\"charset=\".length());\n            }\n        }\n\n        return null;\n    }\n\n    public String getRedirectedUrl() {\n        return redirectedUrl;\n    }\n\n    public void setRedirectedUrl(String redirectUrl) {\n        this.redirectedUrl = redirectUrl;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/IOUtils.java",
    "content": "package io.gonative.android;\n\n\nimport java.io.Closeable;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.OutputStream;\n\nimport io.gonative.gonative_core.GNLog;\n\npublic class IOUtils {\n    private static final String TAG = IOUtils.class.getName();\n\n\tpublic static void copy(InputStream in, OutputStream out) throws IOException{\n\t\tbyte[] buf = new byte[1024];\n\t    int len;\n\t    while ((len = in.read(buf)) > 0) {\n\t        out.write(buf, 0, len);\n\t    }\n\t}\n\t\n\tpublic static void close(Closeable c) {\n        if (c == null) return;\n        try {\n            c.close();\n        } catch (IOException e){\n            GNLog.getInstance().logError(TAG, e.toString(), e);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/Installation.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.content.pm.ApplicationInfo;\nimport android.content.pm.PackageInfo;\nimport android.content.pm.PackageManager;\nimport android.os.Build;\nimport android.telephony.SubscriptionInfo;\nimport android.telephony.SubscriptionManager;\nimport android.util.Log;\n\nimport androidx.core.app.ActivityCompat;\n\nimport java.io.File;\nimport java.io.FileOutputStream;\nimport java.io.IOException;\nimport java.io.RandomAccessFile;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.TimeZone;\nimport java.util.UUID;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 8/8/14.\n */\npublic class Installation {\n    private static final String TAG = Installation.class.getName();\n\n    private static String sID = null;\n    private static final String INSTALLATION = \"INSTALLATION\";\n\n    public synchronized static String id(Context context) {\n        if (sID == null) {\n            File installation = new File(context.getFilesDir(), INSTALLATION);\n            try {\n                if (!installation.exists())\n                    writeInstallationFile(installation);\n                sID = readInstallationFile(installation);\n            } catch (Exception e) {\n                throw new RuntimeException(e);\n            }\n        }\n        return sID;\n    }\n\n    public static Map<String,Object> getInfo(Context context) {\n        HashMap<String,Object> info = new HashMap<>();\n\n        info.put(\"platform\", \"android\");\n\n        String publicKey = AppConfig.getInstance(context).publicKey;\n        if (publicKey == null) publicKey = \"\";\n        info.put(\"publicKey\", publicKey);\n\n        String packageName = context.getPackageName();\n        info.put(\"appId\", packageName);\n\n\n        PackageManager manager = context.getPackageManager();\n        try {\n            PackageInfo packageInfo = manager.getPackageInfo(packageName, 0);\n            info.put(\"appVersion\", packageInfo.versionName);\n            info.put(\"appVersionCode\", packageInfo.versionCode);\n        } catch (PackageManager.NameNotFoundException e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n\n        String distribution;\n        boolean isDebuggable =  ( 0 != ( context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE ) );\n        if (isDebuggable) {\n            distribution = \"debug\";\n        } else {\n            String installer = manager.getInstallerPackageName(packageName);\n            if (installer == null) {\n                distribution = \"adhoc\";\n            } else if (installer.equals(\"com.android.vending\") || installer.equals(\"com.google.market\")) {\n                distribution = \"playstore\";\n            } else if (installer.equals(\"com.amazon.venezia\")) {\n                distribution = \"amazon\";\n            } else {\n                distribution = installer;\n            }\n        }\n        info.put(\"distribution\", distribution);\n\n        info.put(\"language\", Locale.getDefault().getLanguage());\n        info.put(\"os\", \"Android\");\n        info.put(\"osVersion\", Build.VERSION.RELEASE);\n        info.put(\"model\", Build.MANUFACTURER + \" \" + Build.MODEL);\n        info.put(\"hardware\", Build.FINGERPRINT);\n        info.put(\"timeZone\", TimeZone.getDefault().getID());\n        info.put(\"deviceName\", getDeviceName());\n\n        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {\n            SubscriptionManager subscriptionManager = SubscriptionManager.from(context);\n\n            if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED) {\n                List<String> carriers = new ArrayList<>();\n                for (SubscriptionInfo subscriptionInfo : subscriptionManager.getActiveSubscriptionInfoList()) {\n                    carriers.add(subscriptionInfo.getCarrierName().toString());\n                }\n                info.put(\"carrierNames\", carriers);\n                try {\n                    info.put(\"carrierName\", carriers.get(0));\n                } catch ( IndexOutOfBoundsException e ) {\n                    Log.w(TAG, \"getInfo: No carriers registered with subscription manager\");\n                }\n            } else {\n                Log.w(TAG, \"getInfo: Cannot get carrierNames, READ_PHONE_STATE not granted\");\n            }\n        }\n\n        info.put(\"installationId\", Installation.id(context));\n\n        return info;\n    }\n\n    private static String readInstallationFile(File installation) throws IOException {\n        RandomAccessFile f = new RandomAccessFile(installation, \"r\");\n        byte[] bytes = new byte[(int) f.length()];\n        f.readFully(bytes);\n        f.close();\n        return new String(bytes);\n    }\n\n    private static void writeInstallationFile(File installation) throws IOException {\n        FileOutputStream out = new FileOutputStream(installation);\n        String id = UUID.randomUUID().toString();\n        out.write(id.getBytes());\n        out.close();\n    }\n\n    private static String getDeviceName() {\n        String manufacturer = Build.MANUFACTURER;\n        String model = Build.MODEL;\n        String name;\n        if (model.startsWith(manufacturer)) {\n            name = model;\n        } else {\n            name = manufacturer + \" \" + model;\n        }\n        return name;\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/JsCustomCodeExecutor.java",
    "content": "package io.gonative.android;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.Map;\n\nimport io.gonative.gonative_core.GNLog;\n\npublic class JsCustomCodeExecutor {\n    private static final String TAG = JsCustomCodeExecutor.class.getName();\n\n    public static interface CustomCodeHandler {\n        JSONObject execute(Map<String, String> params);\n    }\n\n    // The default CustomCodeHandler \"Echo\"\n    // Simply maps all the key/values of the given params into a JSONObject\n    private static CustomCodeHandler handler = new CustomCodeHandler() {\n        @Override\n        public JSONObject execute(Map<String, String> params) {\n            if(params != null) {\n                JSONObject json = new JSONObject();\n                try {\n                    for(Map.Entry<String, String> entry : params.entrySet()) {\n                        json.put(entry.getKey(), entry.getValue());\n                    }\n                }\n                catch(JSONException e) {\n                    GNLog.getInstance().logError(TAG, \"Error building custom Json Data\", e);\n                }\n                return json;\n            }\n            return null;\n        }\n    };\n\n    /**\n     * Set new CustomCodeHandler to override the default \"Echo\" handler\n     * @param customHandler\n     */\n    public static void setHandler(CustomCodeHandler customHandler) {\n        if(customHandler == null)\n            return;\n        handler = customHandler;\n    }\n\n    /**\n     * Code Handler gets triggered by the UrlNavigation class\n     *\n     * @param params A map consisting of all URI parameters and their values\n     * @return A JSONObject as defined by the Code Handler\n     *\n     * @see UrlNavigation#shouldOverrideUrlLoading\n     */\n    public static JSONObject execute(Map<String, String> params) {\n        try {\n            return handler.execute(params);\n        } catch(Exception e) {\n            GNLog.getInstance().logError(TAG, \"Error executing custom code\", e);\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/JsResultBridge.java",
    "content": "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",
    "content": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Color;\nimport android.graphics.drawable.Drawable;\nimport android.graphics.drawable.GradientDrawable;\nimport android.graphics.drawable.StateListDrawable;\nimport android.util.Pair;\nimport android.view.LayoutInflater;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.widget.BaseExpandableListAdapter;\nimport android.widget.ExpandableListView;\nimport android.widget.ImageView;\nimport android.widget.RelativeLayout;\nimport android.widget.TextView;\n\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport io.gonative.android.icons.Icon;\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 4/14/14.\n */\npublic class JsonMenuAdapter extends BaseExpandableListAdapter\n        implements ExpandableListView.OnGroupClickListener, ExpandableListView.OnChildClickListener {\n    private static final String TAG = JsonMenuAdapter.class.getName();\n\n    private MainActivity mainActivity;\n    private JSONArray menuItems;\n\n    private boolean groupsHaveIcons = false;\n    private boolean childrenHaveIcons = false;\n    private String status;\n    private int selectedIndex;\n    private ExpandableListView expandableListView;\n    private Integer highlightColor;\n    private int sidebar_icon_size;\n    private int sidebar_expand_indicator_size;\n\n    JsonMenuAdapter(MainActivity activity, ExpandableListView expandableListView) {\n        this.mainActivity = activity;\n        sidebar_icon_size = mainActivity.getResources().getInteger(R.integer.sidebar_icon_size);\n        sidebar_expand_indicator_size = mainActivity.getResources().getInteger(R.integer.sidebar_expand_indicator_size);\n        this.expandableListView = expandableListView;\n        menuItems = null;\n\n        this.highlightColor = activity.getResources().getColor(R.color.sidebarHighlight);\n\n        // broadcast messages\n        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (intent.getAction() != null && intent.getAction().equals(AppConfig.PROCESSED_MENU_MESSAGE)) {\n                    update();\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this.mainActivity)\n                .registerReceiver(broadcastReceiver,\n                        new IntentFilter(AppConfig.PROCESSED_MENU_MESSAGE));\n\n    }\n\n    private synchronized void update() {\n        update(this.status);\n    }\n\n    public synchronized void update(String status) {\n        if (status == null) status = \"default\";\n        this.status = status;\n\n        menuItems = AppConfig.getInstance(mainActivity).menus.get(status);\n        if (menuItems == null) menuItems = new JSONArray();\n\n        // figure out groupsHaveIcons and childrenHaveIcons (for layout alignment)\n        groupsHaveIcons = false;\n        childrenHaveIcons = false;\n        for (int i = 0; i < menuItems.length(); i++) {\n            JSONObject item = menuItems.optJSONObject(i);\n            if (item == null) continue;\n\n            if (!item.isNull(\"icon\") && !item.optString(\"icon\").isEmpty()) {\n                groupsHaveIcons = true;\n            }\n\n            if (item.optBoolean(\"isGrouping\", false)) {\n                JSONArray sublinks = item.optJSONArray(\"subLinks\");\n                if (sublinks != null) {\n                    for (int j = 0; j < sublinks.length(); j++) {\n                        JSONObject sublink = sublinks.optJSONObject(j);\n                        if (sublink != null && !sublink.isNull(\"icon\") && !sublink.optString(\"icon\").isEmpty()) {\n                            childrenHaveIcons = true;\n                            break;\n                        }\n                    }\n                }\n\n            }\n        }\n\n        notifyDataSetChanged();\n    }\n\n\n    private String itemString(String s, int groupPosition) {\n        String value = null;\n        try {\n            JSONObject section = (JSONObject) menuItems.get(groupPosition);\n            if (!section.isNull(s))\n                value = section.getString(s).trim();\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n\n        return value;\n    }\n\n    private String itemString(String s, int groupPosition, int childPosition) {\n        String value = null;\n        try {\n            JSONObject section = (JSONObject) menuItems.get(groupPosition);\n            JSONObject sublink = section.getJSONArray(\"subLinks\").getJSONObject(childPosition);\n            if (!sublink.isNull(s))\n                value = sublink.getString(s).trim();\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n\n        return value;\n    }\n\n    private String getTitle(int groupPosition) {\n        return itemString(\"label\", groupPosition);\n    }\n\n    private String getTitle(int groupPosition, int childPosition) {\n        return itemString(\"label\", groupPosition, childPosition);\n    }\n\n    private Pair<String, String> getUrlAndJavascript(int groupPosition) {\n        String url = itemString(\"url\", groupPosition);\n        String js = itemString(\"javascript\", groupPosition);\n        return new Pair<>(url, js);\n    }\n\n    private Pair<String, String> getUrlAndJavascript(int groupPosition, int childPosition) {\n        String url = itemString(\"url\", groupPosition, childPosition);\n        String js = itemString(\"javascript\", groupPosition, childPosition);\n        return new Pair<>(url, js);\n    }\n\n    private boolean isGrouping(int groupPosition) {\n        try {\n            JSONObject section = (JSONObject) menuItems.get(groupPosition);\n            return section.optBoolean(\"isGrouping\", false);\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n            return false;\n        }\n    }\n\n    @Override\n    public int getGroupCount() {\n        return menuItems.length();\n    }\n\n    @Override\n    public int getChildrenCount(int groupPosition) {\n        int count = 0;\n        try {\n            JSONObject section = (JSONObject) menuItems.get(groupPosition);\n            if (section.optBoolean(\"isGrouping\", false)) {\n                count = section.getJSONArray(\"subLinks\").length();\n            } else {\n                count = 0;\n            }\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n        return count;\n    }\n\n    @Override\n    public Object getGroup(int i) {\n        return null;\n    }\n\n    @Override\n    public Object getChild(int i, int i2) {\n        return null;\n    }\n\n    @Override\n    public long getGroupId(int i) {\n        return 0;\n    }\n\n    @Override\n    public long getChildId(int i, int i2) {\n        return 0;\n    }\n\n    @Override\n    public boolean hasStableIds() {\n        return false;\n    }\n\n    @Override\n    public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {\n        if (convertView == null) {\n            LayoutInflater inflater = mainActivity.getLayoutInflater();\n\n            convertView = inflater.inflate(groupsHaveIcons ?\n                    R.layout.menu_group_icon : R.layout.menu_group_noicon, null);\n\n\n            TextView title = convertView.findViewById(R.id.menu_item_title);\n            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n\n        }\n        RelativeLayout menuItem = convertView.findViewById(R.id.menu_item);\n        GradientDrawable shape = getHighlightDrawable();\n        StateListDrawable stateListDrawable = new StateListDrawable();\n        stateListDrawable.addState(new int[]{android.R.attr.state_activated}, shape);\n        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, shape);\n\n        menuItem.setBackground(stateListDrawable);\n\n        // expand/collapse indicator\n        ImageView indicator = convertView.findViewById(R.id.menu_group_indicator);\n        if (isGrouping(groupPosition)) {\n            String iconName;\n            int color = Color.BLACK;\n            if (isExpanded) {\n                iconName = \"fas fa-angle-up\";\n            } else {\n                iconName = \"fas fa-angle-down\";\n            }\n\n            if (groupPosition == this.selectedIndex) {\n                color = this.highlightColor;\n            } else {\n                color = mainActivity.getResources().getColor(R.color.sidebarForeground);\n            }\n            indicator.setImageDrawable(new Icon(mainActivity, iconName, sidebar_expand_indicator_size, color).getDrawable());\n\n            indicator.setVisibility(View.VISIBLE);\n        } else {\n            indicator.setVisibility(View.GONE);\n        }\n\n        //set the title\n        TextView title = convertView.findViewById(R.id.menu_item_title);\n        title.setText(getTitle(groupPosition));\n        if (this.selectedIndex == groupPosition) {\n            title.setTextColor(this.highlightColor);\n        } else {\n            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n        }\n\n        // set icon\n        String icon = itemString(\"icon\", groupPosition);\n        ImageView imageView = convertView.findViewById(R.id.menu_item_icon);\n        if (icon != null && !icon.isEmpty()) {\n            int color;\n            if (groupPosition == this.selectedIndex) {\n                color = this.highlightColor;\n            } else {\n                color = mainActivity.getResources().getColor(R.color.sidebarForeground);\n            }\n            Drawable iconDrawable = new Icon(mainActivity, icon, sidebar_icon_size, color).getDrawable();\n\n            imageView.setImageDrawable(iconDrawable);\n            imageView.setVisibility(View.VISIBLE);\n        } else if (imageView != null) {\n            imageView.setVisibility(View.INVISIBLE);\n        }\n\n        return convertView;\n    }\n\n    @Override\n    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,\n                             View convertView, ViewGroup parent) {\n\n        if (convertView == null) {\n            LayoutInflater inflater = mainActivity.getLayoutInflater();\n\n            if (groupsHaveIcons || childrenHaveIcons)\n                convertView = inflater.inflate(R.layout.menu_child_icon, parent, false);\n            else\n                convertView = inflater.inflate(R.layout.menu_child_noicon, parent, false);\n\n            // style it\n            TextView title = convertView.findViewById(R.id.menu_item_title);\n            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n        }\n\n        RelativeLayout menuItem = convertView.findViewById(R.id.menu_item);\n        GradientDrawable shape = getHighlightDrawable();\n        StateListDrawable stateListDrawable = new StateListDrawable();\n        stateListDrawable.addState(new int[]{android.R.attr.state_activated}, shape);\n        stateListDrawable.addState(new int[]{android.R.attr.state_selected}, shape);\n\n        menuItem.setBackground(stateListDrawable);\n\n        // set title\n        TextView title = convertView.findViewById(R.id.menu_item_title);\n        title.setText(getTitle(groupPosition, childPosition));\n        if (this.selectedIndex == (groupPosition + childPosition) + 1) {\n            title.setTextColor(this.highlightColor);\n        } else {\n            title.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n        }\n\n        // set icon\n        String icon = itemString(\"icon\", groupPosition, childPosition);\n        ImageView imageView = convertView.findViewById(R.id.menu_item_icon);\n        if (icon != null && !icon.isEmpty()) {\n            int color;\n            if (this.selectedIndex == (groupPosition + childPosition) + 1) {\n                color = this.highlightColor;\n            } else {\n                color = mainActivity.getResources().getColor(R.color.sidebarForeground);\n            }\n            Drawable iconDrawable = new Icon(mainActivity, icon, sidebar_icon_size, color).getDrawable();\n\n            imageView.setImageDrawable(iconDrawable);\n            imageView.setVisibility(View.VISIBLE);\n        } else if (imageView != null) {\n            imageView.setVisibility(View.INVISIBLE);\n        }\n\n        return convertView;\n    }\n\n    @Override\n    public boolean isChildSelectable(int groupPosition, int childPosition) {\n        return true;\n    }\n\n    @Override\n    public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {\n        try {\n            if (isGrouping(groupPosition)) {\n                // return false for default handling behavior\n                return false;\n            } else {\n                Pair<String,String> urlAndJavascript = getUrlAndJavascript(groupPosition);\n                loadUrlAndJavascript(urlAndJavascript.first, urlAndJavascript.second);\n                return true; // tell android that we have handled it\n            }\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n\n        return false;\n    }\n\n    @Override\n    public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {\n        int index = parent.getFlatListPosition(ExpandableListView.getPackedPositionForChild(groupPosition, childPosition));\n        parent.setItemChecked(index, true);\n        this.selectedIndex = index;\n        Pair<String, String> urlAndJavascript = getUrlAndJavascript(groupPosition, childPosition);\n        loadUrlAndJavascript(urlAndJavascript.first, urlAndJavascript.second);\n        return true;\n    }\n\n    private void loadUrlAndJavascript(String url, String javascript) {\n        // check for GONATIVE_USERID\n        if (UrlInspector.getInstance().getUserId() != null) {\n            url = url.replaceAll(\"GONATIVE_USERID\", UrlInspector.getInstance().getUserId());\n        }\n\n        if (javascript == null) mainActivity.loadUrl(url);\n        else mainActivity.loadUrlAndJavascript(url, javascript);\n\n        mainActivity.closeDrawers();\n    }\n\n    public void autoSelectItem(String url) {\n        String formattedUrl = url.replaceAll(\"/$\", \"\");\n        if (menuItems == null) return;\n\n        for (int i = 0; i < menuItems.length(); i++) {\n            if (formattedUrl.equals(menuItems.optJSONObject(i).optString(\"url\").replaceAll(\"/$\", \"\"))) {\n                expandableListView.setItemChecked(i, true);\n                selectedIndex = i;\n                return;\n            }\n        }\n    }\n\n    private GradientDrawable getHighlightDrawable() {\n        GradientDrawable shape = new GradientDrawable();\n        shape.setCornerRadius(10);\n        shape.setColor(this.highlightColor);\n        shape.setAlpha(30);\n\n        return shape;\n    }\n\n    @Override\n    public int getChildType(int groupPosition, int childPosition) {\n        if (groupsHaveIcons || childrenHaveIcons) return 0;\n        else return 1;\n    }\n\n    @Override\n    public int getChildTypeCount() {\n        return 2;\n    }\n\n    @Override\n    public int getGroupType(int groupPosition) {\n        if (groupsHaveIcons) return 0;\n        else return 1;\n    }\n\n    @Override\n    public int getGroupTypeCount() {\n        return 2;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/KeyboardManager.kt",
    "content": "package io.gonative.android\n\nimport android.graphics.Rect\nimport android.text.TextUtils\nimport android.view.ViewGroup\nimport io.gonative.gonative_core.LeanUtils\nimport org.json.JSONObject\n\n\nclass KeyboardManager(val activity: MainActivity, private val rootLayout: ViewGroup) {\n\n    var callback: String? = \"\"\n    private var keyboardWidth = 0\n    private var keyboardHeight = 0\n    private var screenWidth = 0\n    private var screenHeight = 0\n    private var isKeyboardVisible = false\n    private var screenHeightOffset = 0\n\n    init {\n        rootLayout.viewTreeObserver\n            .addOnGlobalLayoutListener {\n                val r = Rect()\n                rootLayout.getWindowVisibleDisplayFrame(r)\n\n                if (screenHeightOffset == 0) {\n                    screenHeightOffset = rootLayout.rootView.height - r.bottom\n                }\n\n                screenWidth = rootLayout.rootView.width\n                screenHeight = r.bottom + screenHeightOffset\n\n                keyboardHeight = rootLayout.rootView.height - screenHeight\n\n                if (keyboardHeight == screenHeightOffset) {\n                    keyboardHeight = 0\n                }\n\n                val visible =  keyboardHeight != 0\n\n                if (visible) {\n                    keyboardWidth = screenWidth\n                    if (!isKeyboardVisible) {\n                        isKeyboardVisible = true\n                        notifyCallback();\n                    }\n                } else {\n                    keyboardWidth = 0\n                    if (isKeyboardVisible) {\n                        isKeyboardVisible = false\n                        notifyCallback();\n                    }\n                }\n            }\n    }\n\n    private fun notifyCallback() {\n        if (TextUtils.isEmpty(callback)) return\n        activity.runJavascript(LeanUtils.createJsForCallback(callback, getKeyboardData()))\n    }\n\n    fun getKeyboardData() : JSONObject {\n        val keyboardWindowSize = JSONObject()\n        keyboardWindowSize.put(\"visible\", isKeyboardVisible)\n        keyboardWindowSize.put(\"width\", keyboardWidth)\n        keyboardWindowSize.put(\"height\", keyboardHeight)\n\n        val visibleWindowSize = JSONObject()\n        visibleWindowSize.put(\"width\", screenWidth)\n        visibleWindowSize.put(\"height\", screenHeight)\n\n        val data = JSONObject()\n        data.put(\"keyboardWindowSize\", keyboardWindowSize)\n        data.put(\"visibleWindowSize\", visibleWindowSize)\n\n        return data\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/LoginManager.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\n\nimport org.json.JSONObject;\n\nimport java.lang.ref.WeakReference;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.List;\nimport java.util.Observable;\nimport java.util.regex.Pattern;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 3/16/14.\n */\npublic class LoginManager extends Observable {\n    private static final String TAG = LoginManager.class.getName();\n\n    private Context context;\n    private CheckRedirectionTask task = null;\n\n    private boolean loggedIn = false;\n\n    LoginManager(Context context) {\n        this.context = context;\n        checkLogin();\n    }\n\n    public void checkLogin() {\n        if (task != null)\n            task.cancel(true);\n\n        String loginDetectionUrl = AppConfig.getInstance(context).loginDetectionUrl;\n        if (loginDetectionUrl == null) {\n            return;\n        }\n\n        task = new CheckRedirectionTask(this);\n        task.execute(AppConfig.getInstance(context).loginDetectionUrl);\n    }\n\n    public boolean isLoggedIn() {\n        return loggedIn;\n    }\n\n\n    private static class CheckRedirectionTask extends AsyncTask<String, Void, String> {\n        private WeakReference<LoginManager> loginManagerReference;\n\n        public CheckRedirectionTask(LoginManager loginManager) {\n            this.loginManagerReference = new WeakReference<>(loginManager);\n        }\n\n        @Override\n        protected String doInBackground(String... urls){\n            LoginManager loginManager = loginManagerReference.get();\n            if (loginManager == null) return null;\n\n            try {\n                URL parsedUrl = new URL(urls[0]);\n                HttpURLConnection connection = null;\n                boolean wasRedirected;\n                int numRedirects = 0;\n                do {\n                    if (connection != null)\n                        connection.disconnect();\n\n                    connection = (HttpURLConnection) parsedUrl.openConnection();\n                    connection.setInstanceFollowRedirects(true);\n                    connection.setRequestProperty(\"User-Agent\", AppConfig.getInstance(loginManager.context).userAgent);\n\n                    connection.connect();\n                    int responseCode = connection.getResponseCode();\n\n                    if (responseCode == HttpURLConnection.HTTP_MOVED_PERM ||\n                            responseCode == HttpURLConnection.HTTP_MOVED_TEMP) {\n                        wasRedirected = true;\n                        parsedUrl = new URL(parsedUrl, connection.getHeaderField(\"Location\"));\n                        numRedirects++;\n                    } else {\n                        wasRedirected = false;\n                    }\n                } while (!isCancelled() && wasRedirected && numRedirects < 10);\n\n                String finalUrl = connection.getURL().toString();\n                connection.disconnect();\n                return finalUrl;\n\n            } catch (Exception e) {\n                GNLog.getInstance().logError(TAG, e.getMessage(), e);\n                return null;\n            }\n        }\n\n        @Override\n        protected void onPostExecute(String finalUrl) {\n            LoginManager loginManager = loginManagerReference.get();\n            if (loginManager == null) return;\n\n            UrlInspector.getInstance().inspectUrl(finalUrl);\n            String loginStatus;\n\n            if (finalUrl == null) {\n                loginManager.loggedIn = false;\n                loginStatus = \"default\";\n                loginManager.setChanged();\n                loginManager.notifyObservers();\n                return;\n            }\n\n            // iterate through loginDetectionRegexes\n            AppConfig appConfig = AppConfig.getInstance(loginManager.context);\n\n            List<Pattern> regexes = appConfig.loginDetectRegexes;\n            for (int i = 0; i < regexes.size(); i++) {\n                Pattern regex = regexes.get(i);\n                if (regex.matcher(finalUrl).matches()) {\n                    JSONObject entry = appConfig.loginDetectLocations.get(i);\n                    loginManager.loggedIn = entry.optBoolean(\"loggedIn\", false);\n\n                    loginStatus = AppConfig.optString(entry, \"menuName\");\n                    if (loginStatus == null) loginStatus = loginManager.loggedIn ? \"loggedIn\" : \"default\";\n\n                    loginManager.setChanged();\n                    loginManager.notifyObservers();\n                    break;\n                }\n            }\n        }\n\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/MainActivity.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.SuppressLint;\nimport android.content.BroadcastReceiver;\nimport android.content.ClipData;\nimport android.content.ClipboardManager;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.content.SharedPreferences;\nimport android.content.pm.ActivityInfo;\nimport android.content.pm.PackageManager;\nimport android.content.res.Configuration;\nimport android.content.res.Resources;\nimport android.graphics.PorterDuff;\nimport android.hardware.SensorManager;\nimport android.location.LocationManager;\nimport android.net.ConnectivityManager;\nimport android.net.NetworkInfo;\nimport android.net.Uri;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.os.Handler;\nimport android.preference.PreferenceManager;\nimport android.provider.Settings;\nimport android.telephony.PhoneStateListener;\nimport android.telephony.SignalStrength;\nimport android.telephony.TelephonyManager;\nimport android.text.TextUtils;\nimport android.util.Base64;\nimport android.util.Log;\nimport android.view.KeyEvent;\nimport android.view.Menu;\nimport android.view.MenuItem;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.view.ViewParent;\nimport android.view.WindowManager;\nimport android.webkit.CookieManager;\nimport android.webkit.JavascriptInterface;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.widget.ExpandableListView;\nimport android.widget.ImageView;\nimport android.widget.ProgressBar;\nimport android.widget.RelativeLayout;\nimport android.widget.Spinner;\nimport android.widget.TextView;\nimport android.widget.Toast;\n\nimport androidx.activity.result.ActivityResultLauncher;\nimport androidx.activity.result.contract.ActivityResultContracts;\nimport androidx.annotation.NonNull;\nimport androidx.appcompat.app.ActionBar;\nimport androidx.appcompat.app.ActionBarDrawerToggle;\nimport androidx.appcompat.app.AlertDialog;\nimport androidx.appcompat.app.AppCompatActivity;\nimport androidx.appcompat.app.AppCompatDelegate;\nimport androidx.appcompat.widget.Toolbar;\nimport androidx.core.app.ActivityCompat;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.GravityCompat;\nimport androidx.drawerlayout.widget.DrawerLayout;\nimport androidx.fragment.app.DialogFragment;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\nimport androidx.webkit.WebSettingsCompat;\nimport androidx.webkit.WebViewFeature;\n\nimport com.aurelhubert.ahbottomnavigation.AHBottomNavigation;\nimport com.squareup.seismic.ShakeDetector;\n\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.io.File;\nimport java.net.CookieHandler;\nimport java.net.URISyntaxException;\nimport java.nio.charset.StandardCharsets;\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.Map;\nimport java.util.Observable;\nimport java.util.Observer;\nimport java.util.Stack;\nimport java.util.UUID;\nimport java.util.regex.Pattern;\n\nimport io.gonative.android.files.CapturedImageSaver;\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.android.widget.GoNativeDrawerLayout;\nimport io.gonative.android.widget.GoNativeSwipeRefreshLayout;\nimport io.gonative.android.widget.SwipeHistoryNavigationLayout;\nimport io.gonative.android.widget.WebViewContainerView;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.GoNativeActivity;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\nimport io.gonative.gonative_core.LeanUtils;\n\npublic class MainActivity extends AppCompatActivity implements Observer,\n        GoNativeActivity,\n        GoNativeSwipeRefreshLayout.OnRefreshListener,\n        ShakeDetector.Listener,\n        ShakeDialogFragment.ShakeDialogListener {\n    public static final String BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED = \"io.gonative.android.MainActivity.Extra.BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED\";\n    private static final String webviewDatabaseSubdir = \"webviewDatabase\";\n\tprivate static final String TAG = MainActivity.class.getName();\n    public static final String INTENT_TARGET_URL = \"targetUrl\";\n    public static final String EXTRA_WEBVIEW_WINDOW_OPEN = \"io.gonative.android.MainActivity.Extra.WEBVIEW_WINDOW_OPEN\";\n    public static final String EXTRA_NEW_ROOT_URL = \"newRootUrl\";\n    public static final String EXTRA_EXCESS_WINDOW_ID = \"excessWindowId\";\n    public static final String EXTRA_IGNORE_INTERCEPT_MAXWINDOWS = \"ignoreInterceptMaxWindows\";\n\tpublic static final int REQUEST_SELECT_FILE = 100;\n    private static final int REQUEST_PERMISSION_READ_EXTERNAL_STORAGE = 101;\n    private static final int REQUEST_PERMISSION_GEOLOCATION = 102;\n    private static final int REQUEST_PERMISSION_WRITE_EXTERNAL_STORAGE = 103;\n    private static final int REQUEST_PERMISSION_GENERIC = 199;\n    private static final int REQUEST_WEBFORM = 300;\n    public static final int REQUEST_WEB_ACTIVITY = 400;\n    public static final int GOOGLE_SIGN_IN = 500;\n    private static final String ON_RESUME_CALLBACK = \"gonative_app_resumed\";\n\n    private static final String SAVED_STATE_ACTIVITY_ID = \"activityId\";\n    private static final String SAVED_STATE_IS_ROOT = \"isRoot\";\n    private static final String SAVED_STATE_URL_LEVEL = \"urlLevel\";\n    private static final String SAVED_STATE_PARENT_URL_LEVEL = \"parentUrlLevel\";\n    private static final String SAVED_STATE_SCROLL_X = \"scrollX\";\n    private static final String SAVED_STATE_SCROLL_Y = \"scrollY\";\n    private static final String SAVED_STATE_WEBVIEW_STATE = \"webViewState\";\n    private static final String SAVED_STATE_IGNORE_THEME_SETUP = \"ignoreThemeSetup\";\n\n    private boolean isActivityPaused = false;\n\n    private WebViewContainerView mWebviewContainer;\n    private GoNativeWebviewInterface mWebview;\n    private View webviewOverlay;\n    boolean isPoolWebview = false;\n    private Stack<String> backHistory = new Stack<>();\n    private String initialUrl;\n    private boolean sidebarNavigationEnabled = true;\n\n\tprivate ValueCallback<Uri> mUploadMessage;\n    private ValueCallback<Uri[]> uploadMessageLP;\n    private Uri directUploadImageUri;\n\tprivate GoNativeDrawerLayout mDrawerLayout;\n\tprivate View mDrawerView;\n\tprivate ExpandableListView mDrawerList;\n    private ProgressBar mProgress;\n    private MySwipeRefreshLayout swipeRefreshLayout;\n    private SwipeHistoryNavigationLayout swipeNavLayout;\n    private RelativeLayout fullScreenLayout;\n    private JsonMenuAdapter menuAdapter = null;\n\tprivate ActionBarDrawerToggle mDrawerToggle;\n    private AHBottomNavigation bottomNavigationView;\n\tprivate ConnectivityManager cm = null;\n    private ProfilePicker profilePicker = null;\n    private TabManager tabManager;\n    private ActionManager actionManager;\n    private boolean isRoot;\n    private float hideWebviewAlpha = 0.0f;\n    private boolean isFirstHideWebview = true;\n    private boolean webviewIsHidden = false;\n    private Handler handler = new Handler();\n    private Menu mOptionsMenu;\n    private String activityId;\n\n    private Runnable statusChecker = new Runnable() {\n        @Override\n        public void run() {\n            runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    checkReadyStatus();\n                }\n            });\n            handler.postDelayed(statusChecker, 100); // 0.1 sec\n        }\n    };\n    private ShakeDetector shakeDetector = new ShakeDetector(this);\n    private FileDownloader fileDownloader;\n    private FileWriterSharer fileWriterSharer;\n    private boolean startedLoading = false; // document readystate checker\n    private LoginManager loginManager;\n    private RegistrationManager registrationManager;\n    private ConnectivityChangeReceiver connectivityReceiver;\n    private KeyboardManager keyboardManager;\n    private BroadcastReceiver navigationTitlesChangedReceiver;\n    private BroadcastReceiver navigationLevelsChangedReceiver;\n    private BroadcastReceiver webviewLimitReachedReceiver;\n    protected String postLoadJavascript;\n    protected String postLoadJavascriptForRefresh;\n    private Stack<Bundle>previousWebviewStates;\n    private GeolocationPermissionCallback geolocationPermissionCallback;\n    private ArrayList<PermissionsCallbackPair> pendingPermissionRequests = new ArrayList<>();\n    private ArrayList<Intent> pendingStartActivityAfterPermissions = new ArrayList<>();\n    private String connectivityCallback;\n    private String connectivityOnceCallback;\n    private PhoneStateListener phoneStateListener;\n    private SignalStrength latestSignalStrength;\n    private boolean restoreBrightnessOnNavigation = false;\n    private ActivityResultLauncher<String> requestPermissionLauncher;\n    private String deviceInfoCallback = \"\";\n    private boolean flagThemeConfigurationChange = false;\n\n    @Override\n\tprotected void onCreate(Bundle savedInstanceState) {\n        final AppConfig appConfig = AppConfig.getInstance(this);\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        GoNativeWindowManager windowManager = application.getWindowManager();\n\n        if(appConfig.androidFullScreen){\n            toggleFullscreen(true);\n        }\n        // must be done AFTER toggleFullScreen to force screen orientation\n        setScreenOrientationPreference();\n\n        if (appConfig.keepScreenOn) {\n            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);\n        }\n\n        this.hideWebviewAlpha  = appConfig.hideWebviewAlpha;\n\n        // App theme setup\n        ConfigPreferences configPreferences = new ConfigPreferences(this);\n        String appTheme = configPreferences.getAppTheme();\n\n        if (TextUtils.isEmpty(appTheme)) {\n            if (!TextUtils.isEmpty(appConfig.androidTheme)) {\n                appTheme = appConfig.androidTheme;\n            } else {\n                appTheme = \"light\"; // default is 'light' to support apps with no night assets provided\n            }\n            configPreferences.setAppTheme(appTheme);\n        }\n\n        boolean ignoreThemeUpdate = false;\n        if (savedInstanceState != null) {\n            ignoreThemeUpdate = savedInstanceState.getBoolean(SAVED_STATE_IGNORE_THEME_SETUP, false);\n        }\n\n        if (ignoreThemeUpdate) {\n            // Ignore app theme setup cause its already called from function setupAppTheme()\n            Log.d(\"GNDebug\", \"onCreate: configuration change from setupAppTheme(), ignoring theme setup\");\n        } else {\n            if (\"light\".equals(appTheme)) {\n                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);\n            } else if (\"dark\".equals(appTheme)) {\n                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);\n            } else if (\"auto\".equals(appTheme)) {\n                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);\n            } else {\n                // default\n                AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);\n                configPreferences.setAppTheme(\"light\");\n            }\n        }\n\n        super.onCreate(savedInstanceState);\n\n        this.activityId = UUID.randomUUID().toString();\n        this.isRoot = getIntent().getBooleanExtra(\"isRoot\", true);\n        int urlLevel = getIntent().getIntExtra(\"urlLevel\", -1);\n        int parentUrlLevel = getIntent().getIntExtra(\"parentUrlLevel\", -1);\n\n        if (savedInstanceState != null) {\n            this.activityId = savedInstanceState.getString(SAVED_STATE_ACTIVITY_ID, activityId);\n            this.isRoot = savedInstanceState.getBoolean(SAVED_STATE_IS_ROOT, isRoot);\n            urlLevel = savedInstanceState.getInt(SAVED_STATE_URL_LEVEL, urlLevel);\n            parentUrlLevel = savedInstanceState.getInt(SAVED_STATE_PARENT_URL_LEVEL, parentUrlLevel);\n        }\n\n        application.mBridge.onActivityCreate(this, isRoot);\n\n        windowManager.addNewWindow(activityId, isRoot);\n        windowManager.setUrlLevels(activityId, urlLevel, parentUrlLevel);\n\n        if (appConfig.maxWindowsEnabled) {\n            windowManager.setIgnoreInterceptMaxWindows(activityId, getIntent().getBooleanExtra(EXTRA_IGNORE_INTERCEPT_MAXWINDOWS, false));\n        }\n\n        if (isRoot) {\n            initialRootSetup();\n        }\n\n        this.loginManager = application.getLoginManager();\n\n        this.fileWriterSharer = new FileWriterSharer(this);\n        this.fileDownloader = new FileDownloader(this);\n\n        // webview pools\n        application.getWebViewPool().init(this);\n\n\t\tcm = (ConnectivityManager) getSystemService(CONNECTIVITY_SERVICE);\n\n        setContentView(R.layout.activity_gonative);\n\n        mProgress = findViewById(R.id.progress);\n        this.fullScreenLayout = findViewById(R.id.fullscreen);\n\n        swipeRefreshLayout = findViewById(R.id.swipe_refresh);\n        swipeRefreshLayout.setEnabled(appConfig.pullToRefresh);\n        swipeRefreshLayout.setOnRefreshListener(this);\n        swipeRefreshLayout.setCanChildScrollUpCallback(() -> mWebview.getWebViewScrollY() > 0);\n\n        if (isAndroidGestureEnabled()) {\n            appConfig.swipeGestures = false;\n        }\n        swipeNavLayout = findViewById(R.id.swipe_history_nav);\n        swipeNavLayout.setEnabled(appConfig.swipeGestures);\n        swipeNavLayout.setSwipeNavListener(new SwipeHistoryNavigationLayout.OnSwipeNavListener() {\n            @Override\n            public boolean canSwipeLeftEdge() {\n                if (mWebview.getMaxHorizontalScroll() > 0) {\n                    if (mWebview.getScrollX() > 0) return false;\n                }\n                return canGoBack();\n            }\n    \n            @Override\n            public boolean canSwipeRightEdge() {\n                if (mWebview.getMaxHorizontalScroll() > 0) {\n                    if (mWebview.getScrollX() < mWebview.getMaxHorizontalScroll()) return false;\n                }\n                return canGoForward();\n            }\n    \n            @NonNull\n            @Override\n            public String getGoBackLabel() {\n                return \"\";\n            }\n    \n            @Override\n            public boolean navigateBack() {\n                if (appConfig.swipeGestures && canGoBack()) {\n                    goBack();\n                    return true;\n                }\n                return false;\n            }\n    \n            @Override\n            public boolean navigateForward() {\n                if (appConfig.swipeGestures && canGoForward()) {\n                    goForward();\n                    return true;\n                }\n                return false;\n            }\n    \n            @Override\n            public void leftSwipeReachesLimit() {\n        \n            }\n    \n            @Override\n            public void rightSwipeReachesLimit() {\n        \n            }\n\n            @Override\n            public boolean isSwipeEnabled() {\n                return appConfig.swipeGestures;\n            }\n        });\n\n        swipeRefreshLayout.setColorSchemeColors(getResources().getColor(R.color.pull_to_refresh_color));\n        swipeNavLayout.setActiveColor(getResources().getColor(R.color.pull_to_refresh_color));\n        swipeRefreshLayout.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.swipe_nav_background));\n        swipeNavLayout.setBackgroundColor(getResources().getColor(R.color.swipe_nav_background));\n\n        this.webviewOverlay = findViewById(R.id.webviewOverlay);\n        this.mWebviewContainer = this.findViewById(R.id.webviewContainer);\n        this.mWebview = this.mWebviewContainer.getWebview();\n        this.mWebviewContainer.setupWebview(this, isRoot);\n        setupWebviewTheme(appTheme);\n\n        boolean isWebViewStateRestored = false;\n        if (savedInstanceState != null) {\n            Bundle webViewStateBundle = savedInstanceState.getBundle(SAVED_STATE_WEBVIEW_STATE);\n            if (webViewStateBundle != null) {\n                // Restore page and history\n                mWebview.restoreStateFromBundle(webViewStateBundle);\n                isWebViewStateRestored = true;\n            }\n\n            // Restore scroll state\n            int scrollX = savedInstanceState.getInt(SAVED_STATE_SCROLL_X, 0);\n            int scrollY = savedInstanceState.getInt(SAVED_STATE_SCROLL_Y, 0);\n            mWebview.scrollTo(scrollX, scrollY);\n        }\n\n        // profile picker\n        if (isRoot && (appConfig.showActionBar || appConfig.showNavigationMenu)) {\n            setupProfilePicker();\n        }\n\n\t\t// proxy cookie manager for httpUrlConnection (syncs to webview cookies)\n        CookieHandler.setDefault(new WebkitCookieManagerProxy());\n\n\n        this.postLoadJavascript = getIntent().getStringExtra(\"postLoadJavascript\");\n        this.postLoadJavascriptForRefresh = this.postLoadJavascript;\n\n        this.previousWebviewStates = new Stack<>();\n\n        // tab navigation\n        this.bottomNavigationView = findViewById(R.id.bottom_navigation);\n        this.tabManager = new TabManager(this, bottomNavigationView);\n\n        hideTabs();\n\n        Toolbar toolbar = findViewById(R.id.toolbar);\n        // Add action bar if getSupportActionBar() is null\n        // regardless of appConfig.showActionBar value to setup drawers, sidenav\n        if (getSupportActionBar() == null) {\n            // Set Material Toolbar as Action Bar.\n            setSupportActionBar(toolbar);\n        }\n        // Hide action bar if showActionBar is FALSE and showNavigationMenu is FALSE\n        if (!appConfig.showActionBar && !appConfig.showNavigationMenu) {\n            getSupportActionBar().hide();\n        }\n\n        if (!appConfig.showLogoInSideBar && !appConfig.showAppNameInSideBar) {\n            RelativeLayout headerLayout = findViewById(R.id.header_layout);\n            if (headerLayout != null) {\n                headerLayout.setVisibility(View.GONE);\n            }\n        }\n\n        if (!appConfig.showLogoInSideBar) {\n            ImageView appIcon = findViewById(R.id.app_logo);\n            if (appIcon != null) {\n                appIcon.setVisibility(View.GONE);\n            }\n        }\n        TextView appName = findViewById(R.id.app_name);\n        if (appName != null) {\n            if(appConfig.showAppNameInSideBar) {\n                appName.setText(appConfig.appName);\n            } else {\n                appName.setVisibility(View.INVISIBLE);\n            }\n        }\n\n        // actions in action bar\n        this.actionManager = new ActionManager(this);\n        this.actionManager.setupActionBar(isRoot);\n\n        // overflow menu icon color\n        if (toolbar!= null && toolbar.getOverflowIcon() != null) {\n            toolbar.getOverflowIcon().setColorFilter(getResources().getColor(R.color.titleTextColor), PorterDuff.Mode.SRC_ATOP);\n        }\n\n        // load url\n        String url;\n\n        if (isWebViewStateRestored) {\n            // WebView already has loaded URL when function mWebview.restoreStateFromBundle() was called\n            url = mWebview.getUrl();\n        } else {\n            Intent intent = getIntent();\n            url = getUrlFromIntent(intent);\n\n            if (url == null && isRoot) url = appConfig.getInitialUrl();\n            // url from intent (hub and spoke nav)\n            if (url == null) url = intent.getStringExtra(\"url\");\n\n            if (url != null) {\n\n                // let plugins add query params to url before loading to WebView\n                Map<String, String> queries = application.mBridge.getInitialUrlQueryItems(this, isRoot);\n                if (queries != null && !queries.isEmpty()) {\n                    Uri.Builder builder = Uri.parse(url).buildUpon();\n                    for (Map.Entry<String, String> entry : queries.entrySet()) {\n                        builder.appendQueryParameter(entry.getKey(), entry.getValue());\n                    }\n                    url = builder.build().toString();\n                }\n\n                this.initialUrl = url;\n                this.mWebview.loadUrl(url);\n            } else if (intent.getBooleanExtra(EXTRA_WEBVIEW_WINDOW_OPEN, false)) {\n                // no worries, loadUrl will be called when this new web view is passed back to the message\n            } else {\n                GNLog.getInstance().logError(TAG, \"No url specified for MainActivity\");\n            }\n        }\n\n        showNavigationMenu(isRoot && appConfig.showNavigationMenu);\n\n        actionManager.setupTitleDisplayForUrl(url);\n\n        updateStatusBarOverlay(appConfig.enableOverlayInStatusBar);\n        updateStatusBarStyle(appConfig.statusBarStyle);\n\n        this.keyboardManager = new KeyboardManager(this, this.findViewById(android.R.id.content));\n\n        // style sidebar\n        if (mDrawerView != null) {\n            mDrawerView.setBackgroundColor(getResources().getColor(R.color.sidebarBackground));\n        }\n\n        // respond to navigation titles processed\n        this.navigationTitlesChangedReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (AppConfig.PROCESSED_NAVIGATION_TITLES.equals(intent.getAction())) {\n                    String url = mWebview.getUrl();\n                    if (url == null) return;\n                    String title = titleForUrl(url);\n                    if (title != null) {\n                        setTitle(title);\n                    } else {\n                        setTitle(R.string.app_name);\n                    }\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this).registerReceiver(this.navigationTitlesChangedReceiver,\n            new IntentFilter(AppConfig.PROCESSED_NAVIGATION_TITLES));\n\n        this.navigationLevelsChangedReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (AppConfig.PROCESSED_NAVIGATION_LEVELS.equals(intent.getAction())) {\n                    String url = mWebview.getUrl();\n                    if (url == null) return;\n                    int level = urlLevelForUrl(url);\n                    setUrlLevel(level);\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this).registerReceiver(this.navigationLevelsChangedReceiver,\n                new IntentFilter(AppConfig.PROCESSED_NAVIGATION_LEVELS));\n\n        this.webviewLimitReachedReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED.equals(intent.getAction())) {\n\n                    String excessWindowId = intent.getStringExtra(EXTRA_EXCESS_WINDOW_ID);\n                    if (!TextUtils.isEmpty(excessWindowId)) {\n                        if (excessWindowId.equals(activityId)) finish();\n                        return;\n                    }\n\n                    boolean isActivityRoot = getGNWindowManager().isRoot(activityId);\n                    if (!isActivityRoot) {\n                        finish();\n                    }\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this).registerReceiver(this.webviewLimitReachedReceiver,\n                new IntentFilter(BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED));\n    \n\n        application.mBridge.onSendInstallationInfo(this, Installation.getInfo(this), mWebview.getUrl());\n\n        validateGoogleService();\n\n        requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {\n            runGonativeDeviceInfo(deviceInfoCallback, false);\n        });\n    }\n\n    public String getActivityId() {\n        return this.activityId;\n    }\n\n    private void initialRootSetup() {\n        File databasePath = new File(getCacheDir(), webviewDatabaseSubdir);\n        if (databasePath.mkdirs()) {\n            Log.v(TAG, \"databasePath \" + databasePath.toString() + \" exists\");\n        }\n\n        // url inspector\n        UrlInspector.getInstance().init(this);\n\n        // Register launch\n        ConfigUpdater configUpdater = new ConfigUpdater(this);\n        configUpdater.registerEvent();\n\n        // registration service\n        this.registrationManager = ((GoNativeApplication) getApplication()).getRegistrationManager();\n    }\n\n    private void setupProfilePicker() {\n        Spinner profileSpinner = findViewById(R.id.profile_picker);\n        profilePicker = new ProfilePicker(this, profileSpinner);\n\n        Spinner segmentedSpinner = findViewById(R.id.segmented_control);\n        new SegmentedController(this, segmentedSpinner);\n    }\n\n    private void showNavigationMenu(boolean showNavigation) {\n        AppConfig appConfig = AppConfig.getInstance(this);\n        // do the list stuff\n        mDrawerLayout = findViewById(R.id.drawer_layout);\n        mDrawerView = findViewById(R.id.left_drawer);\n        mDrawerList = findViewById(R.id.drawer_list);\n\n        if (showNavigation) {\n\n            // unlock drawer\n            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);\n\n            // set shadow\n            mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);\n\n            mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout,\n                    R.string.drawer_open, R.string.drawer_close) {\n                //Called when a drawer has settled in a completely closed state.\n                public void onDrawerClosed(View view) {\n                    invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()\n                    mDrawerLayout.setDisableTouch(appConfig.swipeGestures && canGoBack());\n                }\n\n                //Called when a drawer has settled in a completely open state.\n                public void onDrawerOpened(View drawerView) {\n                    invalidateOptionsMenu(); // creates call to onPrepareOptionsMenu()\n                    mDrawerLayout.setDisableTouch(false);\n                }\n            };\n\n            mDrawerToggle.setDrawerIndicatorEnabled(true);\n            mDrawerToggle.getDrawerArrowDrawable().setColor(getResources().getColor(R.color.titleTextColor));\n\n            mDrawerLayout.addDrawerListener(mDrawerToggle);\n\n            setupMenu();\n\n            // update the menu\n            if (appConfig.loginDetectionUrl != null) {\n                this.loginManager.addObserver(this);\n            }\n        } else {\n            // lock drawer so it could not be swiped\n            mDrawerLayout.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);\n        }\n    }\n\n    private String getUrlFromIntent(Intent intent) {\n        if (intent == null) return null;\n        // first check intent in case it was created from push notification\n        String targetUrl = intent.getStringExtra(INTENT_TARGET_URL);\n        if (targetUrl != null && !targetUrl.isEmpty()){\n            return targetUrl;\n        }\n\n        if (Intent.ACTION_VIEW.equals(intent.getAction())) {\n            Uri uri = intent.getData();\n            if (uri != null && (uri.getScheme().endsWith(\".http\") || uri.getScheme().endsWith(\".https\"))) {\n                Uri.Builder builder = uri.buildUpon();\n                if (uri.getScheme().endsWith(\".https\")) {\n                    builder.scheme(\"https\");\n                } else if (uri.getScheme().endsWith(\".http\")) {\n                    builder.scheme(\"http\");\n                }\n                return builder.build().toString();\n            } else {\n                return intent.getDataString();\n            }\n        }\n\n        return null;\n    }\n\n    protected void onPause() {\n        super.onPause();\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityPause(this);\n        this.isActivityPaused = true;\n        stopCheckingReadyStatus();\n    \n        if (application.mBridge.pauseWebViewOnActivityPause()) {\n            this.mWebview.onPause();\n        }\n\n        // unregister connectivity\n        if (this.connectivityReceiver != null) {\n            unregisterReceiver(this.connectivityReceiver);\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            CookieManager.getInstance().flush();\n        }\n\n        shakeDetector.stop();\n    }\n\n    @Override\n    protected void onStart() {\n        super.onStart();\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityStart(this);\n        if (AppConfig.getInstance(this).enableWebRTCBluetoothAudio) {\n            AudioUtils.initAudioFocusListener(this);\n        }\n    }\n\n    @Override\n    protected void onResume() {\n        super.onResume();\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityResume(this);\n        this.mWebview.onResume();\n\n        if (isActivityPaused) {\n            this.isActivityPaused = false;\n            runJavascript(LeanUtils.createJsForCallback(ON_RESUME_CALLBACK, null));\n        }\n\n        retryFailedPage();\n        // register to listen for connectivity changes\n        this.connectivityReceiver = new ConnectivityChangeReceiver();\n        registerReceiver(this.connectivityReceiver,\n                new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));\n\n        // check login status\n        this.loginManager.checkLogin();\n\n        if (AppConfig.getInstance(this).shakeToClearCache) {\n            SensorManager sensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);\n            shakeDetector.setSensitivity(ShakeDetector.SENSITIVITY_HARD);\n            shakeDetector.start(sensorManager);\n        }\n    }\n\n    @Override\n    protected void onStop() {\n        super.onStop();\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityStop(this);\n        if (isRoot) {\n            if (AppConfig.getInstance(this).clearCache) {\n                this.mWebview.clearCache(true);\n            }\n        }\n    }\n\n    @Override\n    protected void onDestroy() {\n        super.onDestroy();\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityDestroy(this);\n        application.getWindowManager().removeWindow(activityId);\n\n        if (fileDownloader != null) fileDownloader.unbindDownloadService();\n\n        // destroy webview\n        if (this.mWebview != null) {\n            this.mWebview.stopLoading();\n            // must remove from view hierarchy to destroy\n            ViewGroup parent = (ViewGroup) this.mWebview.getParent();\n            if (parent != null) {\n                parent.removeView((View)this.mWebview);\n            }\n            if (!this.isPoolWebview) this.mWebview.destroy();\n        }\n\n        this.loginManager.deleteObserver(this);\n\n        if (this.navigationTitlesChangedReceiver != null) {\n            LocalBroadcastManager.getInstance(this).unregisterReceiver(this.navigationTitlesChangedReceiver);\n        }\n        if (this.navigationLevelsChangedReceiver != null) {\n            LocalBroadcastManager.getInstance(this).unregisterReceiver(this.navigationLevelsChangedReceiver);\n        }\n        if (this.webviewLimitReachedReceiver != null) {\n            LocalBroadcastManager.getInstance(this).unregisterReceiver(this.webviewLimitReachedReceiver);\n        }\n    }\n    \n    @Override\n    public void onSubscriptionChanged() {\n        if (registrationManager == null) return;\n        registrationManager.subscriptionInfoChanged();\n    }\n    \n    @Override\n    public void launchNotificationActivity(String extra) {\n        Intent mainIntent = new Intent(this, MainActivity.class);\n        mainIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);\n        if (extra != null && !extra.isEmpty()) {\n            mainIntent.putExtra(INTENT_TARGET_URL, extra);\n        }\n        \n        startActivity(mainIntent);\n    }\n\n    private void retryFailedPage() {\n        // skip if webview is currently loading\n        if (this.mWebview.getProgress() < 100) return;\n\n        // skip if webview has a page loaded\n        String currentUrl = this.mWebview.getUrl();\n        if (currentUrl != null && !currentUrl.equals(UrlNavigation.OFFLINE_PAGE_URL)) return;\n\n        // skip if there is nothing in history\n        if (this.backHistory.isEmpty()) return;\n\n        // skip if no network connectivity\n        if (this.isDisconnected()) return;\n\n        // finally, retry loading the page\n        this.loadUrl(this.backHistory.pop());\n    }\n\n    protected void onSaveInstanceState (Bundle outState) {\n        // Saves current WebView's history and URL or loaded page state\n        Bundle webViewOutState = new Bundle();\n        mWebview.saveStateToBundle(webViewOutState);\n        outState.putBundle(SAVED_STATE_WEBVIEW_STATE, webViewOutState);\n\n        // Save other WebView data\n        outState.putString(SAVED_STATE_ACTIVITY_ID, activityId);\n        outState.putBoolean(SAVED_STATE_IS_ROOT, getGNWindowManager().isRoot(activityId));\n        outState.putInt(SAVED_STATE_URL_LEVEL, getGNWindowManager().getUrlLevel(activityId));\n        outState.putInt(SAVED_STATE_PARENT_URL_LEVEL, getGNWindowManager().getParentUrlLevel(activityId));\n        outState.putInt(SAVED_STATE_SCROLL_X, mWebview.getWebViewScrollX());\n        outState.putInt(SAVED_STATE_SCROLL_Y, mWebview.getWebViewScrollY());\n        if (flagThemeConfigurationChange) {\n            outState.putBoolean(SAVED_STATE_IGNORE_THEME_SETUP, true);\n        }\n\n        super.onSaveInstanceState(outState);\n    }\n\n    public void addToHistory(String url) {\n        if (url == null) return;\n\n        if (this.backHistory.isEmpty() || !this.backHistory.peek().equals(url)) {\n            this.backHistory.push(url);\n        }\n\n        checkNavigationForPage(url);\n\n        // this is a little hack to show the webview after going back in history in single-page apps.\n        // We may never get onPageStarted or onPageFinished, hence the webview would be forever\n        // hidden when navigating back in single-page apps. We do, however, get an updatedHistory callback.\n        showWebview(0.3);\n    }\n\n    @Override\n    public void hearShake() {\n        String FRAGMENT_TAG = \"ShakeDialogFragment\";\n        if (getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG) != null) {\n            return;\n        }\n\n        ShakeDialogFragment dialog = new ShakeDialogFragment();\n        dialog.show(getSupportFragmentManager(), FRAGMENT_TAG);\n    }\n\n    @Override\n    public void onClearCache(DialogFragment dialog) {\n        clearWebviewCache();\n        Toast.makeText(this, R.string.cleared_cache, Toast.LENGTH_SHORT).show();\n    }\n\n    public boolean canGoBack() {\n        if (this.mWebview == null) return false;\n        return this.mWebview.canGoBack();\n    }\n\n    public void goBack() {\n        if (this.mWebview == null) return;\n        if (LeanWebView.isCrosswalk()) {\n            // not safe to do for non-crosswalk, as we may never get a page finished callback\n            // for single-page apps\n            hideWebview();\n        }\n\n        this.mWebview.goBack();\n    }\n\n    private boolean canGoForward() {\n        return this.mWebview.canGoForward();\n    }\n\n    private void goForward() {\n        if (LeanWebView.isCrosswalk()) {\n            // not safe to do for non-crosswalk, as we may never get a page finished callback\n            // for single-page apps\n            hideWebview();\n        }\n\n        this.mWebview.goForward();\n    }\n\n    @Override\n    public void sharePage(String optionalUrl, String optionalText) {\n        String shareUrl;\n        String currentUrl = this.mWebview.getUrl();\n        if (optionalUrl == null || optionalUrl.isEmpty()) {\n            shareUrl = currentUrl;\n        } else {\n            try {\n                java.net.URI optionalUri = new java.net.URI(optionalUrl);\n                if (optionalUri.isAbsolute()) {\n                    shareUrl = optionalUrl;\n                } else {\n                    java.net.URI currentUri = new java.net.URI(currentUrl);\n                    shareUrl = currentUri.resolve(optionalUri).toString();\n                }\n            } catch (URISyntaxException e) {\n                shareUrl = optionalUrl;\n            }\n        }\n\n        if (shareUrl == null || shareUrl.isEmpty()) return;\n\n        Intent share = new Intent(Intent.ACTION_SEND);\n        share.setType(\"text/plain\");\n        share.putExtra(Intent.EXTRA_TEXT, shareUrl);\n        if (optionalText != null) {\n            share.putExtra(Intent.EXTRA_SUBJECT, optionalText);\n        }\n        startActivity(Intent.createChooser(share, getString(R.string.action_share)));\n    }\n\n    private void logout() {\n        this.mWebview.stopLoading();\n\n        // log out by clearing all cookies and going to home page\n        clearWebviewCookies();\n \n        updateMenu(false);\n        this.loginManager.checkLogin();\n        this.mWebview.loadUrl(AppConfig.getInstance(this).getInitialUrl());\n    }\n\n    public void loadUrl(String url) {\n        loadUrl(url, false);\n    }\n\n    public void loadUrl(String url, boolean isFromTab) {\n        if (url == null) return;\n\n        this.postLoadJavascript = null;\n        this.postLoadJavascriptForRefresh = null;\n\n        if (url.equalsIgnoreCase(\"gonative_logout\"))\n            logout();\n        else\n            this.mWebview.loadUrl(url);\n\n        if (!isFromTab && this.tabManager != null) this.tabManager.selectTab(url, null);\n    }\n\n    public void loadUrlAndJavascript(String url, String javascript) {\n        loadUrlAndJavascript(url, javascript, false);\n    }\n\n    public void loadUrlAndJavascript(String url, String javascript, boolean isFromTab) {\n        String currentUrl = this.mWebview.getUrl();\n\n        if (url != null && currentUrl != null && url.equals(currentUrl)) {\n//            hideWebview();\n            runJavascript(javascript);\n            this.postLoadJavascriptForRefresh = javascript;\n//            showWebview();\n        } else {\n            this.postLoadJavascript = javascript;\n            this.postLoadJavascriptForRefresh = javascript;\n            this.mWebview.loadUrl(url);\n        }\n\n        if (!isFromTab && this.tabManager != null) this.tabManager.selectTab(url, javascript);\n    }\n\n    public void runJavascript(String javascript) {\n        if (javascript == null) return;\n        this.mWebview.runJavascript(javascript);\n    }\n\n\tpublic boolean isDisconnected(){\n\t\tNetworkInfo ni = cm.getActiveNetworkInfo();\n        return ni == null || !ni.isConnected();\n\t}\n\n\t@Override\n\tpublic void clearWebviewCache() {\n        mWebview.clearCache(true);\n    }\n\n    @Override\n    public void clearWebviewCookies() {\n        CookieManager cookieManager = CookieManager.getInstance();\n        cookieManager.removeAllCookies(aBoolean -> Log.d(TAG, \"clearWebviewCookies: onReceiveValue callback: \" + aBoolean));\n        AsyncTask.THREAD_POOL_EXECUTOR.execute(cookieManager::flush);\n    }\n\n    @Override\n    public void hideWebview() {\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onHideWebview(this);\n\n        if (AppConfig.getInstance(this).disableAnimations) return;\n\n        this.webviewIsHidden = true;\n        mProgress.setAlpha(1.0f);\n        mProgress.setVisibility(View.VISIBLE);\n\n        if (this.isFirstHideWebview) {\n            this.webviewOverlay.setAlpha(1.0f);\n        } else {\n            this.webviewOverlay.setAlpha(1 - this.hideWebviewAlpha);\n        }\n\n        showWebview(10);\n    }\n\n    private void showWebview(double delay) {\n        if (delay > 0) {\n            handler.postDelayed(new Runnable() {\n                @Override\n                public void run() {\n                    showWebview();\n                }\n            }, (int) (delay * 1000));\n        } else {\n            showWebview();\n        }\n    }\n\n    // shows webview with no animation\n    public void showWebviewImmediately() {\n        this.isFirstHideWebview = false;\n        webviewIsHidden = false;\n        startedLoading = false;\n        stopCheckingReadyStatus();\n        this.webviewOverlay.setAlpha(0.0f);\n        this.mProgress.setVisibility(View.INVISIBLE);\n\n        injectCSSviaJavascript();\n        injectJSviaJavascript();\n    }\n\n    @Override\n    public void showWebview() {\n        this.isFirstHideWebview = false;\n        startedLoading = false;\n        stopCheckingReadyStatus();\n\n        if (!webviewIsHidden) {\n            // don't animate if already visible\n            mProgress.setVisibility(View.INVISIBLE);\n            return;\n        }\n\n        injectCSSviaJavascript();\n        injectJSviaJavascript();\n\n        webviewIsHidden = false;\n\n        webviewOverlay.animate().alpha(0.0f)\n                .setDuration(300)\n                .setStartDelay(150);\n\n        mProgress.animate().alpha(0.0f)\n                .setDuration(60);\n    }\n\n    private void injectCSSviaJavascript() {\n        AppConfig appConfig = AppConfig.getInstance(this);\n        if ((appConfig.customCSS == null || appConfig.customCSS.isEmpty())\n                && (appConfig.androidCustomCSS == null || appConfig.androidCustomCSS.isEmpty())) return;\n\n        try {\n            StringBuilder builder = new StringBuilder();\n            if(appConfig.customCSS != null)\n                builder.append(appConfig.customCSS).append(\" \");\n            if(appConfig.androidCustomCSS != null)\n                builder.append(appConfig.androidCustomCSS);\n            String encoded = Base64.encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);\n            String js = \"(function() {\" +\n                    \"var parent = document.getElementsByTagName('head').item(0);\" +\n                    \"var style = document.createElement('style');\" +\n                    \"style.type = 'text/css';\" +\n                    // Tell the browser to BASE64-decode the string into your script !!!\n                    \"style.innerHTML = window.atob('\" + encoded + \"');\" +\n                    \"parent.appendChild(style)\" +\n                    \"})()\";\n            runJavascript(js);\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, \"Error injecting customCSS via javascript\", e);\n        }\n    }\n\n    private void injectJSviaJavascript() {\n        AppConfig appConfig = AppConfig.getInstance(this);\n        if ((appConfig.customJS == null || appConfig.customJS.isEmpty())\n                && (appConfig.androidCustomJS == null || appConfig.androidCustomJS.isEmpty())) return;\n\n        try {\n            StringBuilder builder = new StringBuilder();\n            if(appConfig.customJS != null)\n                builder.append(appConfig.customJS).append(\" \");\n            if(appConfig.androidCustomJS != null)\n                builder.append(appConfig.androidCustomJS);\n\n            String encoded = Base64.encodeToString(builder.toString().getBytes(StandardCharsets.UTF_8), Base64.NO_WRAP);\n            String js = \"javascript:(function() {\" +\n                    \"var parent = document.getElementsByTagName('head').item(0);\" +\n                    \"var script = document.createElement('script');\" +\n                    \"script.type = 'text/javascript';\" +\n                    \"script.innerHTML = window.atob('\" + encoded + \"');\" +\n                    \"parent.appendChild(script)\" +\n                    \"})()\";\n            runJavascript(js);\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, \"Error injecting customJS via javascript\", e);\n        }\n    }\n\n\tpublic void updatePageTitle() {\n        if (AppConfig.getInstance(this).useWebpageTitle) {\n            setTitle(this.mWebview.getTitle());\n        }\n    }\n\n    public void update (Observable sender, Object data) {\n        if (sender instanceof LoginManager) {\n            updateMenu(((LoginManager) sender).isLoggedIn());\n        }\n    }\n\n    @Override\n\tpublic void updateMenu(){\n        this.loginManager.checkLogin();\n\t}\n\n    private void updateMenu(boolean isLoggedIn){\n        if (menuAdapter == null)\n            setupMenu();\n\n        try {\n            if (isLoggedIn)\n                menuAdapter.update(\"loggedIn\");\n            else\n                menuAdapter.update(\"default\");\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n    }\n\n\tprivate boolean isDrawerOpen() {\n        return mDrawerLayout != null && mDrawerLayout.isDrawerOpen(mDrawerView);\n    }\n\n    private void setDrawerEnabled(boolean enabled) {\n        if (!isRoot) return;\n\n        AppConfig appConfig = AppConfig.getInstance(this);\n        if (!appConfig.showNavigationMenu) return;\n\n        if (mDrawerLayout != null) {\n            mDrawerLayout.setDrawerLockMode(enabled ? GoNativeDrawerLayout.LOCK_MODE_UNLOCKED :\n                    GoNativeDrawerLayout.LOCK_MODE_LOCKED_CLOSED);\n        }\n\n        if((sidebarNavigationEnabled || appConfig.showActionBar ) && enabled){\n            Toolbar toolbar = findViewById(R.id.toolbar);\n            if (toolbar != null) {\n                toolbar.setVisibility(View.VISIBLE);\n            }\n        }\n\n        ActionBar actionBar = getSupportActionBar();\n        if (actionBar != null) {\n            actionBar.setDisplayHomeAsUpEnabled(enabled);\n        }\n    }\n\n\tprivate void setupMenu(){\n        menuAdapter = new JsonMenuAdapter(this, mDrawerList);\n        try {\n            menuAdapter.update(\"default\");\n            mDrawerList.setAdapter(menuAdapter);\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, \"Error setting up menu\", e);\n        }\n\n        mDrawerList.setOnGroupClickListener(menuAdapter);\n        mDrawerList.setOnChildClickListener(menuAdapter);\n\t}\n\n\n\t@Override\n\tprotected void onPostCreate(Bundle savedInstanceState) {\n\t\tsuper.onPostCreate(savedInstanceState);\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onPostCreate(this, savedInstanceState, isRoot);\n\n\t\t// Sync the toggle state after onRestoreInstanceState has occurred.\n        if (mDrawerToggle != null)\n\t\t    mDrawerToggle.syncState();\n\t}\n\n    @Override\n    public void onConfigurationChanged(Configuration newConfig) {\n        super.onConfigurationChanged(newConfig);\n        this.actionManager.setupActionBarDisplay();\n\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n     // Pass any configuration change to the drawer toggles\n        if (mDrawerToggle != null)\n            mDrawerToggle.onConfigurationChanged(newConfig);\n//        if (swipeRefreshLayout != null)\n//       TODO     swipeRefreshLayout.onConfigurationChanged(newConfig);\n        application.mBridge.onConfigurationChange(this);\n    }\n\n\t@Override\n    protected void onActivityResult(int requestCode, int resultCode, Intent data) {\n        super.onActivityResult(requestCode, resultCode, data);\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        application.mBridge.onActivityResult(this, requestCode, resultCode, data);\n\n        if (data != null && data.getBooleanExtra(\"exit\", false))\n            finish();\n\n        String url = null;\n        boolean success = false;\n        if (data != null) {\n            url = data.getStringExtra(\"url\");\n            success = data.getBooleanExtra(\"success\", false);\n        }\n\n        if (requestCode == REQUEST_WEBFORM && resultCode == RESULT_OK) {\n            if (url != null)\n                loadUrl(url);\n            else {\n                // go to initialURL without login/signup override\n                this.mWebview.setCheckLoginSignup(false);\n                this.mWebview.loadUrl(AppConfig.getInstance(this).getInitialUrl());\n            }\n\n            if (AppConfig.getInstance(this).showNavigationMenu) {\n                updateMenu(success);\n            }\n        }\n\n        if (requestCode == REQUEST_WEB_ACTIVITY && resultCode == RESULT_OK) {\n            if (url != null) {\n                int urlLevel = data.getIntExtra(\"urlLevel\", -1);\n                int parentUrlLevel = getGNWindowManager().getParentUrlLevel(activityId);\n                if (urlLevel == -1 || parentUrlLevel == -1 || urlLevel > parentUrlLevel) {\n                    // open in this activity\n                    this.postLoadJavascript = data.getStringExtra(\"postLoadJavascript\");\n                    loadUrl(url);\n                } else {\n                    // urlLevel <= parentUrlLevel, so pass up the chain\n                    setResult(RESULT_OK, data);\n                    finish();\n                }\n            }\n        }\n\n        if (requestCode == REQUEST_SELECT_FILE) {\n            if (resultCode != RESULT_OK) {\n                cancelFileUpload();\n                return;\n            }\n\n            // from documents (and video camera)\n            if (data != null && data.getData() != null) {\n                if (mUploadMessage != null) {\n                    mUploadMessage.onReceiveValue(data.getData());\n                    mUploadMessage = null;\n                }\n\n                if (uploadMessageLP != null) {\n                    uploadMessageLP.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(resultCode, data));\n                    uploadMessageLP = null;\n                }\n\n                return;\n            }\n\n            // we may get clip data for multi-select documents\n            if (data != null && data.getClipData() != null) {\n                ClipData clipData = data.getClipData();\n                ArrayList<Uri> files = new ArrayList<>(clipData.getItemCount());\n                for (int i = 0; i < clipData.getItemCount(); i++) {\n                    ClipData.Item item = clipData.getItemAt(i);\n                    if (item.getUri() != null) {\n                        files.add(item.getUri());\n                    }\n                }\n\n                if (mUploadMessage != null) {\n                    // shouldn never happen, but just in case, send the first item\n                    if (files.size() > 0) {\n                        mUploadMessage.onReceiveValue(files.get(0));\n                    } else {\n                        mUploadMessage.onReceiveValue(null);\n                    }\n                    mUploadMessage = null;\n                }\n\n                if (uploadMessageLP != null) {\n                    uploadMessageLP.onReceiveValue(files.toArray(new Uri[files.size()]));\n                    uploadMessageLP = null;\n                }\n\n                return;\n            }\n\n            // from camera\n            if (this.directUploadImageUri != null) {\n                Uri currentCaptureUri = new CapturedImageSaver().saveCapturedBitmap(this, this.directUploadImageUri);\n                if (mUploadMessage != null) {\n                    mUploadMessage.onReceiveValue(currentCaptureUri);\n                    mUploadMessage = null;\n                }\n                if (uploadMessageLP != null) {\n                    uploadMessageLP.onReceiveValue(new Uri[]{currentCaptureUri});\n                    uploadMessageLP = null;\n                }\n                getContentResolver().delete(this.directUploadImageUri, null, null);\n                this.directUploadImageUri = null;\n\n                return;\n            }\n\n            // Should not reach here.\n            cancelFileUpload();\n        }\n    }\n\n    public void cancelFileUpload() {\n        if (mUploadMessage != null) {\n            mUploadMessage.onReceiveValue(null);\n            mUploadMessage = null;\n        }\n\n        if (uploadMessageLP != null) {\n            uploadMessageLP.onReceiveValue(null);\n            uploadMessageLP = null;\n        }\n\n        this.directUploadImageUri = null;\n    }\n\n    @Override\n    protected void onNewIntent(Intent intent) {\n        super.onNewIntent(intent);\n        String url = getUrlFromIntent(intent);\n        if (url != null && !url.isEmpty()) {\n            if (!urlEqualsIgnoreSlash(url, mWebview.getUrl()))\n                loadUrl(url);\n            return;\n        }\n        Log.w(TAG, \"Received intent without url\");\n\n        ((GoNativeApplication) getApplication()).mBridge.onActivityNewIntent(this, intent);\n    }\n\n    private boolean urlEqualsIgnoreSlash(String url1, String url2) {\n        if (url1 == null || url2 == null) return false;\n        if (url1.endsWith(\"/\")) {\n            url1 = url1.substring(0, url1.length() - 1);\n        }\n        if (url2.endsWith(\"/\")) {\n            url2 = url2.substring(0, url2.length() - 1);\n        }\n        if (url1.startsWith(\"http://\")) {\n            url1 = \"https://\" + url1.substring(7);\n        }\n        return url1.equals(url2);\n    }\n\n    @Override\n\tpublic boolean onKeyDown(int keyCode, KeyEvent event) {\n\t\tif ((keyCode == KeyEvent.KEYCODE_BACK)) {\n\t\t    if (AppConfig.getInstance(this).disableBackButton) {\n\t\t        return true;\n            }\n\n            if (this.mWebview.exitFullScreen()) {\n                return true;\n            }\n\n\t\t\tif (isDrawerOpen()){\n\t\t\t\tmDrawerLayout.closeDrawers();\n\t\t\t\treturn true;\n\t\t\t}\n            else if (canGoBack()) {\n                goBack();\n                return true;\n            }\n            else if (!this.previousWebviewStates.isEmpty()) {\n                Bundle state = previousWebviewStates.pop();\n                LeanWebView webview = new LeanWebView(this);\n                webview.restoreStateFromBundle(state);\n                switchToWebview(webview, /* isPool */ false, /* isBack */ true);\n                return true;\n            }\n\t\t}\n\n        if (((GoNativeApplication) getApplication()).mBridge.onKeyDown(keyCode, event)) {\n            return true;\n        }\n\n\t\treturn super.onKeyDown(keyCode, event);\n\t}\n\n    // isPoolWebView is used to keep track of whether we are showing a pooled webview, which has implications\n    // for page navigation, namely notifying the pool to disown the webview.\n    // isBack means the webview is being switched in as part of back navigation behavior. If isBack=false,\n    // then we will save the state of the old one switched out.\n    public void switchToWebview(GoNativeWebviewInterface newWebview, boolean isPoolWebview, boolean isBack) {\n        this.mWebviewContainer.setupWebview(this, isRoot);\n\n        // scroll to top\n        ((View)newWebview).scrollTo(0, 0);\n\n        View prev = (View)this.mWebview;\n\n        if (!isBack) {\n            // save the state for back button behavior\n            Bundle stateBundle = new Bundle();\n            this.mWebview.saveStateToBundle(stateBundle);\n            this.previousWebviewStates.add(stateBundle);\n        }\n\n        // replace the current web view in the parent with the new view\n        if (newWebview != prev) {\n            // a view can only have one parent, and attempting to add newWebview if it already has\n            // a parent will cause a runtime exception. So be extra safe by removing it from its parent.\n            ViewParent temp = newWebview.getParent();\n            if (temp instanceof  ViewGroup) {\n                ((ViewGroup) temp).removeView((View)newWebview);\n            }\n\n            ViewGroup parent = (ViewGroup) prev.getParent();\n            int index = parent.indexOfChild(prev);\n            parent.removeView(prev);\n            parent.addView((View) newWebview, index);\n            ((View)newWebview).setLayoutParams(prev.getLayoutParams());\n\n            // webviews can still send some extraneous events to this activity if we do not remove\n            // its callbacks\n            WebViewSetup.removeCallbacks((LeanWebView) prev);\n\n            if (!this.isPoolWebview) {\n                ((GoNativeWebviewInterface)prev).destroy();\n            }\n        }\n\n        this.isPoolWebview = isPoolWebview;\n        this.mWebview = newWebview;\n\n        if (this.postLoadJavascript != null) {\n            runJavascript(this.postLoadJavascript);\n            this.postLoadJavascript = null;\n        }\n    }\n\n\t@Override\n\tpublic boolean onCreateOptionsMenu(Menu menu) {\n\t\t// Inflate the menu; this adds items to the action bar if it is present.\n\t\tgetMenuInflater().inflate(R.menu.topmenu, menu);\n        mOptionsMenu = menu;\n\n        if (this.actionManager != null) {\n            this.actionManager.addActions(menu);\n        }\n\n\t\treturn true;\n\t}\n\n    public Menu getOptionsMenu () {\n        return mOptionsMenu;\n    }\n\n\tpublic void setMenuItemsVisible (boolean visible) {\n        setMenuItemsVisible(visible, null);\n    }\n\n\tpublic void setMenuItemsVisible(boolean visible, MenuItem exception) {\n\n        for (int i = 0; i < mOptionsMenu.size(); i++) {\n            MenuItem item = mOptionsMenu.getItem(i);\n            if (item == exception) {\n                continue;\n            }\n\n            item.setVisible(visible);\n            item.setEnabled(visible);\n        }\n    }\n\n\t@Override\n\tpublic boolean onOptionsItemSelected(MenuItem item) {\n        // Pass the event to ActionBarDrawerToggle, if it returns\n        // true, then it has handled the app icon touch event\n\n        if (mDrawerToggle != null) {\n            if (mDrawerToggle.onOptionsItemSelected(item)) {\n              return true;\n            }\n        }\n\n        // actions\n        if (this.actionManager != null) {\n            if (this.actionManager.onOptionsItemSelected(item)) {\n                return true;\n            }\n        }\n\n        // handle other items\n        if (item.getItemId() == android.R.id.home) {\n            if (this.actionManager.isOnSearchMode()) {\n                this.actionManager.closeSearchView();\n                this.actionManager.setOnSearchMode(false);\n                return true;\n            }\n            finish();\n            return true;\n        }\n        return super.onOptionsItemSelected(item);\n    }\n    \n    @Override\n    public void onRefresh() {\n        refreshPage();\n        stopNavAnimation(true, 1000);\n    }\n    \n    private void stopNavAnimation(boolean isConsumed){\n        stopNavAnimation(isConsumed, 100);\n    }\n    \n    private void stopNavAnimation(boolean isConsumed, int delay){\n        // let the refreshing spinner stay for a little bit if the native show/hide is disabled\n        // otherwise there isn't enough of a user confirmation that the page is refreshing\n        if (isConsumed && AppConfig.getInstance(this).disableAnimations) {\n            new Handler().postDelayed(new Runnable() {\n                @Override\n                public void run() {\n                    swipeRefreshLayout.setRefreshing(false);\n                }\n            }, delay);\n        } else {\n            this.swipeRefreshLayout.setRefreshing(false);\n        }\n    }\n\n    public void refreshPage() {\n        String url = this.mWebview.getUrl();\n        if (url != null && url.equals(UrlNavigation.OFFLINE_PAGE_URL)){\n            if (this.mWebview.canGoBack()) {\n                this.mWebview.goBack();\n            } else if (this.initialUrl != null) {\n                this.mWebview.loadUrl(this.initialUrl);\n            }\n            updateMenu();\n        }\n        else {\n            this.postLoadJavascript = this.postLoadJavascriptForRefresh;\n            this.mWebview.loadUrl(url);\n        }\n    }\n\n    // onPageFinished\n    @Override\n    public void checkNavigationForPage(String url) {\n        // don't change anything on navigation if the url that just finished was a file download\n        if (url.equals(this.fileDownloader.getLastDownloadedUrl())) return;\n\n        if (this.tabManager != null) {\n            this.tabManager.checkTabs(url);\n        }\n\n        if (this.actionManager != null) {\n            this.actionManager.checkActions(url);\n        }\n\n        if (this.registrationManager != null) {\n            this.registrationManager.checkUrl(url);\n        }\n\n        if (this.menuAdapter != null) {\n            this.menuAdapter.autoSelectItem(url);\n        }\n    }\n\n    // onPageStarted\n    @Override\n    public void checkPreNavigationForPage(String url) {\n        if (this.tabManager != null) {\n            this.tabManager.autoSelectTab(url);\n        }\n\n        if (this.menuAdapter != null) {\n            this.menuAdapter.autoSelectItem(url);\n        }\n\n        if (this.actionManager != null) {\n            this.actionManager.cleanSidebarMenuTitleOffset();\n        }\n\n        AppConfig appConfig = AppConfig.getInstance(this);\n        setDrawerEnabled(appConfig.shouldShowSidebarForUrl(url) && sidebarNavigationEnabled);\n\n        // When current URL canGoBack and swipeGestures are enabled, disable touch events on DrawerLayout\n        if (this.mDrawerLayout != null && this.mDrawerLayout.getDrawerLockMode(GravityCompat.START) != DrawerLayout.LOCK_MODE_LOCKED_CLOSED) {\n            mDrawerLayout.setDisableTouch(appConfig.swipeGestures && canGoBack());\n        }\n    }\n\n    public ActionManager getActionManager() {\n        return this.actionManager;\n    }\n\n    @Override\n    public void setupTitleDisplayForUrl(String url) {\n        if (this.actionManager == null) return;\n        this.actionManager.setupTitleDisplayForUrl(url);\n    }\n\n    @Override\n    public int urlLevelForUrl(String url) {\n        ArrayList<Pattern> entries = AppConfig.getInstance(this).navStructureLevelsRegex;\n        if (entries != null) {\n            for (int i = 0; i < entries.size(); i++) {\n                Pattern regex = entries.get(i);\n                if (regex.matcher(url).matches()) {\n                    return AppConfig.getInstance(this).navStructureLevels.get(i);\n                }\n            }\n        }\n\n        // return unknown\n        return -1;\n    }\n\n    @Override\n    public String titleForUrl(String url) {\n        ArrayList<HashMap<String,Object>> entries = AppConfig.getInstance(this).navTitles;\n        String title = null;\n\n        if (entries != null) {\n            for (HashMap<String,Object> entry : entries) {\n                Pattern regex = (Pattern)entry.get(\"regex\");\n\n                if (regex.matcher(url).matches()) {\n                    if (entry.containsKey(\"title\")) {\n                        title = (String)entry.get(\"title\");\n                    }\n                }\n            }\n        }\n\n        return title;\n    }\n\n    public void closeDrawers() {\n        mDrawerLayout.closeDrawers();\n    }\n\n    public boolean isNotRoot() {\n        return !isRoot;\n    }\n\n    @Override\n    public int getParentUrlLevel() {\n        return getGNWindowManager().getParentUrlLevel(activityId);\n    }\n\n    @Override\n    public int getUrlLevel() {\n        return getGNWindowManager().getUrlLevel(activityId);\n    }\n\n    @Override\n    public void setUrlLevel(int urlLevel) {\n        getGNWindowManager().setUrlLevel(activityId, urlLevel);\n    }\n\n    public ProfilePicker getProfilePicker() {\n        return profilePicker;\n    }\n\n    public FileDownloader getFileDownloader() {\n        return fileDownloader;\n    }\n\n    public FileWriterSharer getFileWriterSharer() {\n        return fileWriterSharer;\n    }\n\n    public StatusCheckerBridge getStatusCheckerBridge() {\n        return new StatusCheckerBridge();\n    }\n\n    @Override\n    public void setTitle(CharSequence title) {\n        super.setTitle(title);\n        if (actionManager != null) {\n            actionManager.showTextActionBarTitle(title);\n        }\n    }\n\n    @Override\n    public void startCheckingReadyStatus() {\n        statusChecker.run();\n    }\n\n    private void stopCheckingReadyStatus() {\n        handler.removeCallbacks(statusChecker);\n    }\n\n    private void checkReadyStatus() {\n        this.mWebview.runJavascript(\"if (gonative_status_checker && typeof gonative_status_checker.onReadyState === 'function') gonative_status_checker.onReadyState(document.readyState);\");\n    }\n\n    private void checkReadyStatusResult(String status) {\n        // if interactiveDelay is specified, then look for readyState=interactive, and show webview\n        // with a delay. If not specified, wait for readyState=complete.\n        double interactiveDelay = AppConfig.getInstance(this).interactiveDelay;\n\n        if (status.equals(\"loading\") || (Double.isNaN(interactiveDelay) && status.equals(\"interactive\"))) {\n            startedLoading = true;\n        }\n        else if ((!Double.isNaN(interactiveDelay) && status.equals(\"interactive\"))\n                || (startedLoading && status.equals(\"complete\"))) {\n\n            if (status.equals(\"interactive\")) {\n                showWebview(interactiveDelay);\n            } else {\n                showWebview();\n            }\n        }\n    }\n\n    public void showTabs() {\n        this.bottomNavigationView.setVisibility(View.VISIBLE);\n    }\n\n    public void hideTabs() {\n        this.bottomNavigationView.setVisibility(View.GONE);\n    }\n\n    @Override\n    public void toggleFullscreen(boolean fullscreen) {\n        ActionBar actionBar = this.getSupportActionBar();\n        View decorView = getWindow().getDecorView();\n        int visibility = decorView.getSystemUiVisibility();\n        int fullscreenFlags = View.SYSTEM_UI_FLAG_LOW_PROFILE |\n                View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;\n\n        if (Build.VERSION.SDK_INT >= 16) {\n            fullscreenFlags |= View.SYSTEM_UI_FLAG_FULLSCREEN |\n                    View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;\n        }\n\n        if (Build.VERSION.SDK_INT >= 19) {\n            fullscreenFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE |\n                    View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;\n        }\n\n        if (fullscreen) {\n            visibility |= fullscreenFlags;\n            if (actionBar != null) actionBar.hide();\n        } else {\n            visibility &= ~fullscreenFlags;\n            if (actionBar != null && AppConfig.getInstance(this).showActionBar) actionBar.show();\n\n            // Fix for webview keyboard not showing, see https://github.com/mozilla-tw/FirefoxLite/issues/842\n            this.mWebview.clearFocus();\n        }\n\n        decorView.setSystemUiVisibility(visibility);\n\n        // Full-screen is used for playing videos.\n        // Allow sensor-based rotation when in full screen (even overriding user rotation preference)\n        // If orientation is forced landscape don't set sensor based orientation\n        if (fullscreen && AppConfig.getInstance(this).forceScreenOrientation != AppConfig.ScreenOrientations.LANDSCAPE) {\n            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);\n        } else {\n            setScreenOrientationPreference();\n        }\n    }\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\n        ((GoNativeApplication) getApplication()).mBridge.onRequestPermissionsResult(this, requestCode, permissions, grantResults);\n        switch (requestCode) {\n            case REQUEST_PERMISSION_GEOLOCATION:\n                if (this.geolocationPermissionCallback != null) {\n                    if (grantResults.length >= 2 &&\n                            grantResults[0] == PackageManager.PERMISSION_GRANTED &&\n                            grantResults[1] == PackageManager.PERMISSION_GRANTED) {\n                        this.geolocationPermissionCallback.onResult(true);\n                    } else {\n                        this.geolocationPermissionCallback.onResult(false);\n                    }\n                    this.geolocationPermissionCallback = null;\n                }\n                break;\n            case REQUEST_PERMISSION_GENERIC:\n                Iterator<PermissionsCallbackPair> it = pendingPermissionRequests.iterator();\n                while (it.hasNext()) {\n                    PermissionsCallbackPair pair = it.next();\n                    if (pair.permissions.length != permissions.length) continue;\n                    boolean skip = false;\n                    for (int i = 0; i < pair.permissions.length && i < permissions.length; i++) {\n                        if (!pair.permissions[i].equals(permissions[i])) {\n                            skip = true;\n                            break;\n                        }\n                    }\n                    if (skip) continue;\n\n                    // matches PermissionsCallbackPair\n                    if (pair.callback != null) {\n                        pair.callback.onPermissionResult(permissions, grantResults);\n                    }\n                    it.remove();\n                }\n\n                if (pendingPermissionRequests.size() == 0 && pendingStartActivityAfterPermissions.size() > 0) {\n                    Iterator<Intent> i = pendingStartActivityAfterPermissions.iterator();\n                    while (i.hasNext()) {\n                        Intent intent = i.next();\n                        startActivity(intent);\n                        i.remove();\n                    }\n                }\n                break;\n        }\n    }\n\n    public GoNativeWindowManager getGNWindowManager() {\n        return ((GoNativeApplication) getApplication()).getWindowManager();\n    }\n\n    @Override\n    public int getWindowCount() {\n        return getGNWindowManager().getWindowCount();\n    }\n\n    public void setUploadMessage(ValueCallback<Uri> mUploadMessage) {\n        this.mUploadMessage = mUploadMessage;\n    }\n\n    public void setUploadMessageLP(ValueCallback<Uri[]> uploadMessageLP) {\n        this.uploadMessageLP = uploadMessageLP;\n    }\n\n    public void setDirectUploadImageUri(Uri directUploadImageUri) {\n        this.directUploadImageUri = directUploadImageUri;\n    }\n\n    public RelativeLayout getFullScreenLayout() {\n        return fullScreenLayout;\n    }\n\n    @Override\n    public GoNativeWebviewInterface getWebView() {\n        return mWebview;\n    }\n\n    public class StatusCheckerBridge {\n        @JavascriptInterface\n        public void onReadyState(final String state) {\n            runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    checkReadyStatusResult(state);\n                }\n            });\n        }\n    }\n\n    private class ConnectivityChangeReceiver extends BroadcastReceiver {\n        @Override\n        public void onReceive(Context context, Intent intent) {\n            retryFailedPage();\n            if (connectivityCallback != null) {\n                sendConnectivity(connectivityCallback);\n            }\n        }\n    }\n\n    public void getRuntimeGeolocationPermission(final GeolocationPermissionCallback callback) {\n        if (isLocationPermissionGranted()) {\n            callback.onResult(true);\n        }\n\n        if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_FINE_LOCATION) ||\n                ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.ACCESS_COARSE_LOCATION)) {\n            Toast.makeText(this, R.string.request_permission_explanation_geolocation, Toast.LENGTH_SHORT).show();\n        }\n\n        this.geolocationPermissionCallback = callback;\n        ActivityCompat.requestPermissions(this, new String[]{\n                Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION\n        }, REQUEST_PERMISSION_GEOLOCATION);\n    }\n\n    public void getPermission(String[] permissions, PermissionCallback callback) {\n        boolean needToRequest = false;\n        for (String permission : permissions) {\n            if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) {\n                needToRequest = true;\n                break;\n            }\n        }\n\n        if (needToRequest) {\n            if (callback != null) {\n                pendingPermissionRequests.add(new PermissionsCallbackPair(permissions, callback));\n            }\n\n            ActivityCompat.requestPermissions(this, permissions, REQUEST_PERMISSION_GENERIC);\n        } else {\n            // send all granted result\n            if (callback != null) {\n                int[] results = new int[permissions.length];\n                for (int i = 0; i < results.length; i++) {\n                    results[i] = PackageManager.PERMISSION_GRANTED;\n                }\n                callback.onPermissionResult(permissions, results);\n            }\n        }\n    }\n\n    public void startActivityAfterPermissions(Intent intent) {\n        if (pendingPermissionRequests.size() == 0) {\n            startActivity(intent);\n        } else {\n            pendingStartActivityAfterPermissions.add(intent);\n        }\n    }\n\n    private void setScreenOrientationPreference() {\n        AppConfig appConfig = AppConfig.getInstance(this);\n        if (appConfig.forceScreenOrientation != null) {\n            setDeviceOrientation(appConfig.forceScreenOrientation);\n            return;\n        }\n\n        if (getResources().getBoolean(R.bool.isTablet)) {\n            if (appConfig.tabletScreenOrientation != null) {\n                setDeviceOrientation(appConfig.tabletScreenOrientation);\n                return;\n            }\n        } else {\n            if (appConfig.phoneScreenOrientation != null) {\n                setDeviceOrientation(appConfig.phoneScreenOrientation);\n                return;\n            }\n        }\n\n        if (!appConfig.androidFullScreen) {\n            setDeviceOrientation(AppConfig.ScreenOrientations.UNSPECIFIED);\n        }\n    }\n\n    @SuppressLint(\"SourceLockedOrientationActivity\")\n    private void setDeviceOrientation(AppConfig.ScreenOrientations orientation) {\n        switch (orientation) {\n            case UNSPECIFIED:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);\n                break;\n            case PORTRAIT:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);\n                break;\n            case LANDSCAPE:\n                setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);\n                break;\n        }\n    }\n\n    public TabManager getTabManager() {\n        return tabManager;\n    }\n\n    public interface PermissionCallback {\n        void onPermissionResult(String[] permissions, int[] grantResults);\n    }\n\n    private class PermissionsCallbackPair {\n        String[] permissions;\n        PermissionCallback callback;\n\n        PermissionsCallbackPair(String[] permissions, PermissionCallback callback) {\n            this.permissions = permissions;\n            this.callback = callback;\n        }\n    }\n\n    public void enableSwipeRefresh() {\n        if (this.swipeRefreshLayout != null) {\n            this.swipeRefreshLayout.setEnabled(true);\n        }\n    }\n\n    public void restoreSwipRefreshDefault() {\n        if (this.swipeRefreshLayout != null) {\n            AppConfig appConfig = AppConfig.getInstance(this);\n            this.swipeRefreshLayout.setEnabled(appConfig.pullToRefresh);\n        }\n    }\n\n    @Override\n    public void deselectTabs() {\n        this.bottomNavigationView.setCurrentItem(AHBottomNavigation.CURRENT_ITEM_NONE);\n    }\n\n    private void listenForSignalStrength() {\n        if (this.phoneStateListener != null) return;\n\n        this.phoneStateListener = new PhoneStateListener() {\n            @Override\n            public void onSignalStrengthsChanged(SignalStrength signalStrength) {\n                latestSignalStrength = signalStrength;\n                sendConnectivityOnce();\n                if (connectivityCallback != null) {\n                    sendConnectivity(connectivityCallback);\n                }\n            }\n        };\n\n        try {\n            TelephonyManager telephonyManager = (TelephonyManager)this.getSystemService(Context.TELEPHONY_SERVICE);\n            if (telephonyManager == null) {\n                GNLog.getInstance().logError(TAG, \"Error getting system telephony manager\");\n            } else {\n                telephonyManager.listen(this.phoneStateListener, PhoneStateListener.LISTEN_SIGNAL_STRENGTHS);\n            }\n        } catch (Exception e) {\n            GNLog.getInstance().logError(TAG, \"Error listening for signal strength\", e);\n        }\n\n    }\n\n    @Override\n    public void sendConnectivityOnce(String callback) {\n        if (callback == null) return;\n\n        this.connectivityOnceCallback = callback;\n        if (this.phoneStateListener != null) {\n            sendConnectivity(callback);\n        } else {\n            listenForSignalStrength();\n            new Handler().postDelayed(new Runnable() {\n                @Override\n                public void run() {\n                    sendConnectivityOnce();\n                }\n            }, 500);\n        }\n    }\n\n    private void sendConnectivityOnce() {\n        if (this.connectivityOnceCallback == null) return;\n        sendConnectivity(this.connectivityOnceCallback);\n        this.connectivityOnceCallback = null;\n    }\n\n    private void sendConnectivity(String callback) {\n        NetworkInfo activeNetwork = cm.getActiveNetworkInfo();\n        boolean connected = activeNetwork != null && activeNetwork.isConnected();\n        String typeString;\n        if (activeNetwork != null) {\n            typeString = activeNetwork.getTypeName();\n        } else {\n            typeString = \"DISCONNECTED\";\n        }\n\n        try {\n            JSONObject data = new JSONObject();\n            data.put(\"connected\", connected);\n            data.put(\"type\", typeString);\n\n            if (this.latestSignalStrength != null) {\n                JSONObject signalStrength = new JSONObject();\n\n                signalStrength.put(\"cdmaDbm\", latestSignalStrength.getCdmaDbm());\n                signalStrength.put(\"cdmaEcio\", latestSignalStrength.getCdmaEcio());\n                signalStrength.put(\"evdoDbm\", latestSignalStrength.getEvdoDbm());\n                signalStrength.put(\"evdoEcio\", latestSignalStrength.getEvdoEcio());\n                signalStrength.put(\"evdoSnr\", latestSignalStrength.getEvdoSnr());\n                signalStrength.put(\"gsmBitErrorRate\", latestSignalStrength.getGsmBitErrorRate());\n                signalStrength.put(\"gsmSignalStrength\", latestSignalStrength.getGsmSignalStrength());\n                if (Build.VERSION.SDK_INT >= 23) {\n                    signalStrength.put(\"level\", latestSignalStrength.getLevel());\n                }\n                data.put(\"cellSignalStrength\", signalStrength);\n            }\n\n            String js = LeanUtils.createJsForCallback(callback, data);\n            runJavascript(js);\n        } catch (JSONException e) {\n            GNLog.getInstance().logError(TAG, \"JSON error sending connectivity\", e);\n        }\n    }\n\n    @Override\n    public void subscribeConnectivity(final String callback) {\n        this.connectivityCallback = callback;\n        listenForSignalStrength();\n        new Handler().postDelayed(new Runnable() {\n            @Override\n            public void run() {\n                sendConnectivity(callback);\n            }\n        }, 500);\n    }\n\n    @Override\n    public void unsubscribeConnectivity() {\n        this.connectivityCallback = null;\n    }\n\n    public interface GeolocationPermissionCallback {\n        void onResult(boolean granted);\n    }\n\n    // set brightness to a negative number to restore default\n    @Override\n    public void setBrightness(float brightness) {\n        WindowManager.LayoutParams layout = getWindow().getAttributes();\n        layout.screenBrightness = brightness;\n        getWindow().setAttributes(layout);\n    }\n\n    @Override\n    public void setSidebarNavigationEnabled(boolean enabled) {\n        sidebarNavigationEnabled = enabled;\n        setDrawerEnabled(enabled);\n    }\n\n    public GoNativeDrawerLayout getDrawerLayout() {\n        return this.mDrawerLayout;\n    }\n\n    public ActionBarDrawerToggle getDrawerToggle() {\n        return this.mDrawerToggle;\n    }\n\n    /**\n     * @param appTheme set to null if will use sharedPreferences\n     */\n\n    @Override\n    public void setupAppTheme(String appTheme) {\n        ConfigPreferences preferences = new ConfigPreferences(this);\n        preferences.setAppTheme(appTheme);\n\n        // Updating app theme on runtime triggers a configuration change and recreates the app\n        // To prevent consecutive calls, ignore theme setup on onCreate() by enabling this flag\n        flagThemeConfigurationChange = true;\n\n        if (\"light\".equals(appTheme)) {\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);\n        } else if (\"dark\".equals(appTheme)) {\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);\n        } else {\n            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);\n        }\n    }\n\n    @SuppressLint(\"RequiresFeature\")\n    private void setupWebviewTheme(String appTheme) {\n        if (!WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {\n            Log.d(TAG, \"Dark mode feature is not supported\");\n            return;\n        }\n\n        if (mWebview.getSettings() == null) {\n            return;\n        }\n\n        if (\"dark\".equals(appTheme)) {\n            WebSettingsCompat.setForceDark(this.mWebview.getSettings(), WebSettingsCompat.FORCE_DARK_ON);\n        } else if (\"light\".equals(appTheme)) {\n            WebSettingsCompat.setForceDark(this.mWebview.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);\n        } else {\n            switch (getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {\n                case Configuration.UI_MODE_NIGHT_YES:\n                    WebSettingsCompat.setForceDark(this.mWebview.getSettings(), WebSettingsCompat.FORCE_DARK_ON);\n                    break;\n                case Configuration.UI_MODE_NIGHT_NO:\n                case Configuration.UI_MODE_NIGHT_UNDEFINED:\n                    WebSettingsCompat.setForceDark(this.mWebview.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);\n                    break;\n            }\n\n            // Force dark on if supported, and only use theme from web\n            if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK_STRATEGY)) {\n                WebSettingsCompat.setForceDarkStrategy(\n                        this.mWebview.getSettings(),\n                        WebSettingsCompat.DARK_STRATEGY_WEB_THEME_DARKENING_ONLY\n                );\n            }\n        }\n    }\n\n    private void validateGoogleService() {\n        try {\n            if (BuildConfig.GOOGLE_SERVICE_INVALID) {\n                Toast.makeText(this, R.string.google_service_required, Toast.LENGTH_LONG).show();\n                GNLog.getInstance().logError(TAG, \"validateGoogleService: \" + R.string.google_service_required, null, GNLog.TYPE_TOAST_ERROR);\n            }\n        } catch (NullPointerException ex) {\n            GNLog.getInstance().logError(TAG, \"validateGoogleService: \" + ex.getMessage(), null, GNLog.TYPE_TOAST_ERROR);\n        }\n    }\n\n    @SuppressLint(\"DiscouragedApi\")\n    private boolean isAndroidGestureEnabled() {\n        if (Build.VERSION.SDK_INT < 29) return false;\n        try {\n            int resourceId = getResources().getIdentifier(\"config_navBarInteractionMode\", \"integer\", \"android\");\n            if (resourceId > 0) {\n                // 0 : Navigation is displaying with 3 buttons\n                // 1 : Navigation is displaying with 2 button(Android P navigation mode)\n                // 2 : Full screen gesture(Gesture on android Q)\n                return getResources().getInteger(resourceId) == 2;\n            }\n            return false;\n        } catch (Resources.NotFoundException ex) {\n            GNLog.getInstance().logError(TAG, \"isAndroidGestureEnabled: \", ex);\n            return false;\n        }\n    }\n\n    @Override\n    public void updateStatusBarOverlay(boolean isOverlayEnabled) {\n        View decor = getWindow().getDecorView();\n        if (isOverlayEnabled) {\n            decor.setSystemUiVisibility(decor.getSystemUiVisibility() |\n                    View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |\n                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n        } else {\n            decor.setSystemUiVisibility(decor.getSystemUiVisibility() &\n                    ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN &\n                    ~View.SYSTEM_UI_FLAG_LAYOUT_STABLE);\n        }\n    }\n\n    @Override\n    public void updateStatusBarStyle(String statusBarStyle) {\n        if (statusBarStyle != null && !statusBarStyle.isEmpty() && Build.VERSION.SDK_INT >= 23) {\n            switch (statusBarStyle) {\n                case \"light\": {\n                    // dark icons and text\n                    View decor = getWindow().getDecorView();\n                    decor.setSystemUiVisibility(decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                    break;\n                }\n                case \"dark\": {\n                    // light icons and text\n                    View decor = getWindow().getDecorView();\n                    decor.setSystemUiVisibility(decor.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                    break;\n                }\n                case \"auto\":\n                    int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;\n                    if (nightModeFlags == Configuration.UI_MODE_NIGHT_YES) {\n                        View decor = getWindow().getDecorView();\n                        decor.setSystemUiVisibility(decor.getSystemUiVisibility() & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                    } else if (nightModeFlags == Configuration.UI_MODE_NIGHT_NO) {\n                        View decor = getWindow().getDecorView();\n                        decor.setSystemUiVisibility(decor.getSystemUiVisibility() | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);\n                    } else {\n                        GNLog.getInstance().logError(TAG, \"updateStatusBarStyle: Current mode is undefined\");\n                    }\n                    break;\n            }\n        }\n    }\n\n    @Override\n    public void setStatusBarColor(int color) {\n        getWindow().setStatusBarColor(color);\n    }\n\n    @Override\n    public void runGonativeDeviceInfo(String callback, boolean includeCarrierNames) {\n        if (includeCarrierNames) {\n            deviceInfoCallback = callback;\n            requestPermissionLauncher.launch(Manifest.permission.READ_PHONE_STATE);\n        } else {\n            Map<String, Object> installationInfo = Installation.getInfo(this);\n            SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);\n            if (!sharedPreferences.getBoolean(\"hasLaunched\", false)) {\n                sharedPreferences.edit().putBoolean(\"hasLaunched\", true).commit();\n                installationInfo.put(\"isFirstLaunch\", true);\n            } else {\n                installationInfo.put(\"isFirstLaunch\", false);\n            }\n\n            JSONObject jsonObject = new JSONObject(installationInfo);\n            String js = LeanUtils.createJsForCallback(callback, jsonObject);\n            this.runJavascript(js);\n        }\n    }\n\n    @Override\n    public void windowFlag(boolean add, int flag) {\n        if (add) {\n            getWindow().addFlags(flag);\n        } else {\n            getWindow().clearFlags(flag);\n        }\n    }\n\n    @Override\n    public void setCustomTitle(String title) {\n        if (!title.isEmpty()) {\n            setTitle(title);\n        } else {\n            setTitle(R.string.app_name);\n        }\n    }\n\n    @Override\n    public void downloadFile(String url, String filename, boolean shouldSaveToGallery, boolean open) {\n        fileDownloader.downloadFile(url, filename, shouldSaveToGallery, open);\n    }\n\n    @Override\n    public void selectTab(int tabNumber) {\n        if (tabManager == null) return;\n        tabManager.selectTabNumber(tabNumber, false);\n    }\n\n    @Override\n    public void setTabsWithJson(JSONObject tabsJson, int tabMenuId) {\n        if (tabManager == null) return;\n        tabManager.setTabsWithJson(tabsJson, tabMenuId);\n    }\n\n    @Override\n    public void focusAudio(boolean enabled) {\n        if (enabled) {\n            AudioUtils.requestAudioFocus(this);\n        } else {\n            AudioUtils.abandonFocusRequest(this);\n        }\n    }\n\n    @Override\n    public void clipboardSet(String content) {\n        if (content.isEmpty()) return;\n        ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n        ClipData clip = ClipData.newPlainText(\"copy\", content);\n        clipboard.setPrimaryClip(clip);\n    }\n\n    @Override\n    public void clipboardGet(String callback) {\n        if (!TextUtils.isEmpty(callback)) {\n            Map<String, String> params = new HashMap<>();\n            ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);\n            CharSequence pasteData;\n            if (clipboard.hasPrimaryClip()) {\n                ClipData.Item item = clipboard.getPrimaryClip().getItemAt(0);\n                pasteData = item.getText();\n                if (pasteData != null)\n                    params.put(\"data\", pasteData.toString());\n                else\n                    params.put(\"error\", \"Clipboard item is not a string.\");\n            } else {\n                params.put(\"error\", \"No Clipboard item available.\");\n            }\n            JSONObject jsonObject = new JSONObject(params);\n            runJavascript(LeanUtils.createJsForCallback(callback, jsonObject));\n        }\n    }\n\n    @Override\n    public void sendRegistration(JSONObject data) {\n        if(registrationManager == null) return;\n\n        if(data != null){\n            JSONObject customData = data.optJSONObject(\"customData\");\n            if(customData == null){\n                try { // try converting json string from url to json object\n                    customData = new JSONObject(data.optString(\"customData\"));\n                } catch (JSONException e){\n                    GNLog.getInstance().logError(TAG, \"GoNative Registration JSONException:- \" + e.getMessage(), e);\n                }\n            }\n            if(customData != null){\n                registrationManager.setCustomData(customData);\n            }\n        }\n        registrationManager.sendToAllEndpoints();\n    }\n\n    @Override\n    public void runCustomNativeBridge(Map<String, String> params) {\n        // execute code defined by the CustomCodeHandler\n        // call JsCustomCodeExecutor#setHandler to override this default handler\n        JSONObject data = JsCustomCodeExecutor.execute(params);\n        String callback = params.get(\"callback\");\n        if(callback != null && !callback.isEmpty()) {\n            final String js = LeanUtils.createJsForCallback(callback, data);\n            // run on main thread\n            Handler mainHandler = new Handler(getMainLooper());\n            mainHandler.post(() -> runJavascript(js));\n        }\n    }\n\n    @Override\n    public void promptLocationService() {\n        getRuntimeGeolocationPermission(granted -> Log.d(TAG, \"promptLocationService: \" + granted));\n    }\n\n    @Override\n    public boolean isLocationServiceEnabled() {\n\n        if (!isLocationPermissionGranted()) {\n            return false;\n        }\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            LocationManager lm = getSystemService(LocationManager.class);\n            return lm.isLocationEnabled();\n        } else {\n            // This is Deprecated in API 28\n            int mode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF);\n            return  (mode != Settings.Secure.LOCATION_MODE_OFF);\n        }\n    }\n\n    private boolean isLocationPermissionGranted() {\n            int checkFine = ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_FINE_LOCATION);\n            int checkCoarse = ContextCompat.checkSelfPermission(this, android.Manifest.permission.ACCESS_COARSE_LOCATION);\n            return checkFine == PackageManager.PERMISSION_GRANTED && checkCoarse == PackageManager.PERMISSION_GRANTED;\n    }\n\n    @Override\n    public void setRestoreBrightnessOnNavigation(boolean restore) {\n        this.restoreBrightnessOnNavigation = restore;\n    }\n\n    public boolean isRestoreBrightnessOnNavigation() {\n        return this.restoreBrightnessOnNavigation;\n    }\n\n\n\n    public Object getJavascriptBridge() {\n        GoNativeApplication application = (GoNativeApplication)getApplication();\n        return application.mBridge.getJavaScriptBridge();\n    }\n\n    @Override\n    public void closeCurrentWindow() {\n        if (!getGNWindowManager().isRoot(activityId)) {\n            this.finish();\n        }\n    }\n\n    @Override\n    public void openNewWindow(String url) {\n        if (TextUtils.isEmpty(url)) return;\n        AppConfig appConfig = AppConfig.getInstance(this);\n\n        // Check maxWindows conditions\n        if (appConfig.maxWindowsEnabled && appConfig.numWindows > 0 && getGNWindowManager().getWindowCount() >= appConfig.numWindows && onMaxWindowsReached(url))\n            return;\n\n        Intent intent = new Intent(this, MainActivity.class);\n        intent.putExtra(\"isRoot\", false);\n        intent.putExtra(\"url\", url);\n        intent.putExtra(MainActivity.EXTRA_IGNORE_INTERCEPT_MAXWINDOWS, true);\n        startActivityForResult(intent, MainActivity.REQUEST_WEB_ACTIVITY);\n    }\n\n    @Override      \n    public boolean onMaxWindowsReached(String url) {\n        AppConfig appConfig = AppConfig.getInstance(this);\n        GoNativeWindowManager windowManager = getGNWindowManager();\n\n        if (appConfig.autoClose && LeanUtils.urlsMatchIgnoreTrailing(url, appConfig.getInitialUrl())) {\n\n            // Set this activity as new root\n            isRoot = true;\n\n            windowManager.setAsNewRoot(activityId);\n\n            // Reset URL levels\n            windowManager.setUrlLevels(activityId, -1, -1);\n\n            // Reload activity as root\n            initialRootSetup();\n            if (appConfig.showActionBar || appConfig.showNavigationMenu) {\n                setupProfilePicker();\n            }\n\n            showNavigationMenu(appConfig.showNavigationMenu);\n\n            if (actionManager != null) {\n                actionManager.setupActionBar(isRoot);\n                actionManager.setupTitleDisplayForUrl(url);\n            }\n\n            if (mDrawerToggle != null && appConfig.showNavigationMenu) {\n                mDrawerToggle.syncState();\n            }\n\n            windowManager.setIgnoreInterceptMaxWindows(activityId, true);\n\n            // Send broadcast to close other activity\n            Intent intent = new Intent(MainActivity.BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED);\n            intent.putExtra(MainActivity.EXTRA_NEW_ROOT_URL, url);\n            LocalBroadcastManager.getInstance(this).sendBroadcast(intent);\n\n            // Add listener when all excess windows are closed\n            windowManager.setOnExcessWindowClosedListener(() -> {\n                // Load new URL\n                mWebview.loadUrl(url);\n                // Remove listener\n                windowManager.setOnExcessWindowClosedListener(null);\n            });\n\n            return true;\n        } else {\n\n            // Get excess window\n            String excessWindowId = windowManager.getExcessWindow();\n\n            // Send broadcast to close the excess window\n            Intent intent = new Intent(MainActivity.BROADCAST_RECEIVER_ACTION_WEBVIEW_LIMIT_REACHED);\n            intent.putExtra(MainActivity.EXTRA_EXCESS_WINDOW_ID, excessWindowId);\n            LocalBroadcastManager.getInstance(this).sendBroadcast(intent);\n\n            // Remove from window list\n            windowManager.removeWindow(excessWindowId);\n        }\n\n        return false;\n    }\n\n    @Override\n    public void getKeyboardInfo(String callback) {\n        if (keyboardManager == null || TextUtils.isEmpty(callback)) return;\n        runJavascript(LeanUtils.createJsForCallback(callback, keyboardManager.getKeyboardData()));\n    }\n\n    @Override\n    public void addKeyboardListener(String callback) {\n        if (keyboardManager == null) return;\n        keyboardManager.setCallback(callback);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/MySwipeRefreshLayout.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\n\nimport io.gonative.android.widget.GoNativeSwipeRefreshLayout;\n\n/**\n * Created by weiyin on 9/13/15.\n * Copyright 2014 GoNative.io LLC\n */\npublic class MySwipeRefreshLayout extends GoNativeSwipeRefreshLayout {\n    private CanChildScrollUpCallback canChildScrollUpCallback;\n\n    public interface CanChildScrollUpCallback {\n        boolean canSwipeRefreshChildScrollUp();\n    }\n\n    public MySwipeRefreshLayout(Context context) {\n        super(context);\n    }\n\n    public MySwipeRefreshLayout(Context context, AttributeSet attrs) {\n        super(context, attrs);\n    }\n\n    public void setCanChildScrollUpCallback(CanChildScrollUpCallback canChildScrollUpCallback) {\n        this.canChildScrollUpCallback = canChildScrollUpCallback;\n    }\n\n    @Override\n    public boolean canChildScrollUp() {\n        if (canChildScrollUpCallback != null) {\n            return canChildScrollUpCallback.canSwipeRefreshChildScrollUp();\n        } else {\n            return super.canChildScrollUp();\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ProfilePicker.java",
    "content": "package io.gonative.android;\n\nimport androidx.annotation.NonNull;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.JavascriptInterface;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.Spinner;\nimport android.widget.TextView;\n\nimport org.json.JSONArray;\nimport org.json.JSONException;\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\n\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 5/9/14.\n */\npublic class ProfilePicker implements AdapterView.OnItemSelectedListener {\n    private static final String TAG = ProfilePicker.class.getName();\n\n    private MainActivity mainActivity;\n    private JSONArray json;\n    private ArrayList<String> names;\n    private ArrayList<String> links;\n    private int selectedIndex;\n\n    private ArrayAdapter<String> adapter;\n    private Spinner spinner;\n    private ProfileJsBridge profileJsBridge;\n\n    public ProfilePicker(MainActivity mainActivity, Spinner spinner) {\n        this.mainActivity = mainActivity;\n        this.spinner = spinner;\n        this.names = new ArrayList<>();\n        this.links = new ArrayList<>();\n        this.spinner.setAdapter(getAdapter());\n        this.spinner.setOnItemSelectedListener(this);\n        this.profileJsBridge = new ProfileJsBridge();\n    }\n\n    private void parseJson(String s){\n        try {\n            json = new JSONArray(s);\n            this.names.clear();\n            this.links.clear();\n\n            for (int i = 0; i < json.length(); i++) {\n                JSONObject item = json.getJSONObject(i);\n\n                this.names.add(item.optString(\"name\", \"\"));\n                this.links.add(item.optString(\"link\", \"\"));\n\n                if (item.optBoolean(\"selected\", false)){\n                    selectedIndex = i;\n                }\n            }\n\n            mainActivity.runOnUiThread(new Runnable() {\n                public void run() {\n                    if (selectedIndex < ProfilePicker.this.names.size()) {\n                        ProfilePicker.this.spinner.setSelection(selectedIndex);\n                    }\n                    if (ProfilePicker.this.json != null &&\n                            ProfilePicker.this.json.length() > 0)\n                        ProfilePicker.this.spinner.setVisibility(View.VISIBLE);\n                    else\n                        ProfilePicker.this.spinner.setVisibility(View.GONE);\n                    getAdapter().notifyDataSetChanged();\n                }\n            });\n\n        } catch (JSONException e) {\n            GNLog.getInstance().logError(TAG, e.getMessage(), e);\n        }\n    }\n\n    private ArrayAdapter<String> getAdapter(){\n        if (adapter == null) {\n\n            adapter = new ArrayAdapter<String>(mainActivity, R.layout.profile_picker_dropdown, names) {\n                @NonNull\n                @Override\n                public View getView(int position, View convertView, @NonNull ViewGroup parent) {\n                    TextView view = (TextView) super.getView(position, convertView, parent);\n                    view.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n                    return view;\n                }\n\n                @Override\n                public View getDropDownView(int position, View convertView, @NonNull ViewGroup parent) {\n                    TextView view = (TextView) super.getDropDownView(position, convertView, parent);\n                    view.setTextColor(mainActivity.getResources().getColor(R.color.sidebarForeground));\n                    return view;\n                }\n            };\n        }\n\n        return adapter;\n    }\n\n    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n        // only load if selection has changed\n        if (position != selectedIndex) {\n            mainActivity.loadUrl(links.get(position));\n            mainActivity.closeDrawers();\n            selectedIndex = position;\n        }\n    }\n\n    public void onNothingSelected(AdapterView<?> parent) {\n        // do nothing\n    }\n\n    public ProfileJsBridge getProfileJsBridge() {\n        return profileJsBridge;\n    }\n\n    public class ProfileJsBridge {\n        @JavascriptInterface\n        public void parseJson(String s) {\n            ProfilePicker.this.parseJson(s);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/RegistrationManager.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\nimport android.os.AsyncTask;\nimport android.util.Log;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.OutputStreamWriter;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.util.HashMap;\nimport java.util.Iterator;\nimport java.util.LinkedList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.LeanUtils;\n\n/**\n * Created by weiyin on 10/4/15.\n */\npublic class RegistrationManager {\n    private final static String TAG = RegistrationManager.class.getName();\n\n    private Context context;\n    private JSONObject customData;\n    private String lastUrl;\n\n    private List<RegistrationEndpoint> registrationEndpoints;\n\n    RegistrationManager(Context context) {\n        this.context = context;\n        this.registrationEndpoints = new LinkedList<>();\n    }\n\n    public void processConfig(JSONArray endpoints) {\n        registrationEndpoints.clear();\n\n        if (endpoints == null) return;\n\n        for (int i = 0; i < endpoints.length(); i++) {\n            JSONObject endpoint = endpoints.optJSONObject(i);\n            if (endpoint == null) continue;\n\n            String url = LeanUtils.optString(endpoint, \"url\");\n            if (url == null) {\n                Log.w(TAG, \"Invalid registration: endpoint url is null\");\n                continue;\n            }\n\n            List<Pattern> urlRegexes = LeanUtils.createRegexArrayFromStrings(endpoint.opt(\"urlRegex\"));\n\n            RegistrationEndpoint registrationEndpoint = new RegistrationEndpoint(url, urlRegexes);\n            registrationEndpoints.add(registrationEndpoint);\n        }\n    }\n\n    public void checkUrl(String url) {\n        this.lastUrl = url;\n        for (RegistrationEndpoint endpoint : registrationEndpoints) {\n            if (LeanUtils.stringMatchesAnyRegex(url, endpoint.urlRegexes)) {\n                endpoint.sendRegistrationInfo();\n            }\n        }\n    }\n\n    public void setCustomData(JSONObject customData) {\n        this.customData = customData;\n        registrationDataChanged();\n    }\n\n    public void sendToAllEndpoints() {\n        for (RegistrationEndpoint endpoint : registrationEndpoints) {\n                endpoint.sendRegistrationInfo();\n        }\n    }\n\n    private void registrationDataChanged() {\n        for (RegistrationEndpoint endpoint : registrationEndpoints) {\n            if (this.lastUrl != null &&\n                    LeanUtils.stringMatchesAnyRegex(this.lastUrl, endpoint.urlRegexes)) {\n                endpoint.sendRegistrationInfo();\n            }\n        }\n    }\n\n    public void subscriptionInfoChanged(){\n        registrationDataChanged();\n    }\n\n    private class RegistrationEndpoint {\n        private String postUrl;\n        private List<Pattern> urlRegexes;\n\n        RegistrationEndpoint(String postUrl, List<Pattern> urlRegexes) {\n            this.postUrl = postUrl;\n            this.urlRegexes = urlRegexes;\n        }\n\n        void sendRegistrationInfo() {\n            new SendRegistrationTask(context, this, RegistrationManager.this).execute();\n        }\n    }\n\n    private static class SendRegistrationTask extends AsyncTask<Void,Void,Void> {\n        private RegistrationEndpoint registrationEndpoint;\n        private RegistrationManager registrationManager;\n        private Context context;\n\n        SendRegistrationTask(Context context, RegistrationEndpoint registrationEndpoint, RegistrationManager registrationManager) {\n            this.registrationEndpoint = registrationEndpoint;\n            this.registrationManager = registrationManager;\n            this.context = context;\n        }\n\n        @Override\n        protected Void doInBackground(Void... voids) {\n            Map<String, Object> toSend = new HashMap<>();\n\n            toSend.putAll(Installation.getInfo(registrationManager.context));\n\n            // Append provider info to Map toSend\n            if (((GoNativeApplication) context).getAnalyticsProviderInfo() != null) {\n                toSend.putAll(((GoNativeApplication) context).getAnalyticsProviderInfo());\n            }\n\n            if (registrationManager.customData != null) {\n                Iterator<String> keys = registrationManager.customData.keys();\n                while(keys.hasNext()) {\n                    String key = keys.next();\n                    toSend.put(\"customData_\" + key, registrationManager.customData.opt(key));\n                }\n            }\n\n            try {\n                JSONObject json = new JSONObject(toSend);\n\n                URL url = new URL(registrationEndpoint.postUrl);\n                HttpURLConnection connection = (HttpURLConnection) url.openConnection();\n                connection.setRequestMethod(\"POST\");\n                connection.setRequestProperty(\"Content-Type\", \"application/json\");\n                connection.setDoOutput(true);\n                OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), \"UTF-8\");\n                writer.write(json.toString());\n                writer.close();\n                connection.connect();\n                int result = connection.getResponseCode();\n\n                if (result < 200 || result > 299) {\n                    Log.w(TAG, \"Recevied status code \" + result + \" when posting to \" + registrationEndpoint.postUrl);\n                }\n            } catch (Exception e) {\n                GNLog.getInstance().logError(TAG, \"Error posting to \" + registrationEndpoint.postUrl, e);\n            }\n\n            return null;\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/SegmentedController.java",
    "content": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\nimport android.view.View;\nimport android.widget.AdapterView;\nimport android.widget.ArrayAdapter;\nimport android.widget.Spinner;\n\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\n\nimport io.gonative.gonative_core.AppConfig;\n\n/**\n * Created by weiyin on 12/20/15.\n * Copyright 2014 GoNative.io LLC\n */\npublic class SegmentedController implements AdapterView.OnItemSelectedListener {\n    private MainActivity mainActivity;\n    private ArrayList<String> labels;\n    private ArrayList<String> urls;\n    private int selectedIndex;\n\n    private ArrayAdapter<String> adapter;\n    private Spinner spinner;\n\n    SegmentedController(MainActivity mainActivity, Spinner spinner) {\n        this.mainActivity = mainActivity;\n        this.spinner = spinner;\n\n        this.labels = new ArrayList<>();\n        this.urls = new ArrayList<>();\n\n        this.spinner.setAdapter(getAdapter());\n        this.spinner.setOnItemSelectedListener(this);\n\n        BroadcastReceiver messageReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (intent == null || intent.getAction() == null) return;\n\n                if (intent.getAction().equals(AppConfig.PROCESSED_SEGMENTED_CONTROL)) {\n                    updateSegmentedControl();\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this.mainActivity).registerReceiver(\n                messageReceiver, new IntentFilter(AppConfig.PROCESSED_SEGMENTED_CONTROL));\n\n        updateSegmentedControl();\n    }\n\n    private void updateSegmentedControl() {\n        this.labels.clear();\n        this.urls.clear();\n        this.selectedIndex = -1;\n\n        AppConfig appConfig = AppConfig.getInstance(mainActivity);\n        if (appConfig.segmentedControl == null) return;\n\n        for (int i = 0; i < appConfig.segmentedControl.size(); i++) {\n            JSONObject item = appConfig.segmentedControl.get(i);\n\n            String label = item.optString(\"label\", \"Invalid\");\n            String url = item.optString(\"url\", \"\");\n            Boolean selected = item.optBoolean(\"selected\");\n\n            this.labels.add(i, label);\n            this.urls.add(i, url);\n            if (selected) this.selectedIndex = i;\n        }\n\n        mainActivity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                if (selectedIndex > -1) {\n                    spinner.setSelection(selectedIndex);\n                }\n\n                if (labels.size() > 0) {\n                    spinner.setVisibility(View.VISIBLE);\n                } else {\n                    spinner.setVisibility(View.GONE);\n                }\n\n                adapter.notifyDataSetChanged();\n            }\n        });\n\n    }\n\n    private ArrayAdapter<String> getAdapter() {\n        if (this.adapter != null) {\n            return this.adapter;\n        }\n\n        ArrayAdapter<String> adapter =  new ArrayAdapter<>(mainActivity,\n                android.R.layout.simple_spinner_item, labels);\n        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);\n        this.adapter = adapter;\n        return adapter;\n    }\n\n    @Override\n    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {\n        // only load if selection has changed\n        if (position != selectedIndex) {\n            String url = urls.get(position);\n\n            if (url != null && url.length() > 0) {\n                mainActivity.loadUrl(url);\n            }\n\n            mainActivity.closeDrawers();\n            selectedIndex = position;\n        }\n    }\n\n    @Override\n    public void onNothingSelected(AdapterView<?> parent) {\n        // do nothing\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/ShakeDialogFragment.java",
    "content": "package io.gonative.android;\n\nimport android.app.AlertDialog;\nimport android.app.Dialog;\nimport android.content.Context;\nimport android.content.DialogInterface;\nimport android.os.Bundle;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.fragment.app.DialogFragment;\n\npublic class ShakeDialogFragment extends DialogFragment {\n    public interface ShakeDialogListener {\n        public void onClearCache(DialogFragment dialog);\n    }\n\n    ShakeDialogListener listener;\n\n    @NonNull\n    @Override\n    public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {\n        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());\n        builder.setTitle(R.string.shake_to_clear_cache)\n                .setItems(R.array.device_shaken_options, new DialogInterface.OnClickListener() {\n                    @Override\n                    public void onClick(DialogInterface dialogInterface, int i) {\n                        if (i == 0) {\n                            listener.onClearCache(ShakeDialogFragment.this);\n                        }\n                    }\n                });\n        return builder.create();\n    }\n\n    @Override\n    public void onAttach(Context context) {\n        super.onAttach(context);\n        try {\n            listener = (ShakeDialogListener) context;\n        } catch (ClassCastException e) {\n            throw new ClassCastException(context.toString() + \" must implement ShakeDialogListener\");\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/SplashActivity.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.content.Intent;\nimport android.content.pm.PackageManager;\nimport android.graphics.Color;\nimport android.os.Bundle;\n\nimport android.os.Handler;\nimport android.os.Looper;\nimport android.view.View;\n\nimport androidx.annotation.NonNull;\nimport androidx.core.app.ActivityCompat;\nimport androidx.appcompat.app.AppCompatActivity;\n\nimport java.util.HashSet;\n\nimport androidx.core.content.ContextCompat;\nimport androidx.core.splashscreen.SplashScreen;\n\nimport io.gonative.gonative_core.AppConfig;\n\npublic class SplashActivity extends AppCompatActivity {\n    private static final int REQUEST_STARTUP_PERMISSIONS = 100;\n\n    @Override\n    protected void onCreate(Bundle savedInstanceState) {\n        SplashScreen.installSplashScreen(this);\n        super.onCreate(savedInstanceState);\n        AppConfig config = AppConfig.getInstance(this);\n\n        getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);\n        getWindow().setStatusBarColor(Color.TRANSPARENT);\n        getWindow().setNavigationBarColor(Color.TRANSPARENT);\n        setContentView(R.layout.splash_screen);\n\n        HashSet<String> permissionsToRequest = new HashSet<>();\n\n        if (LeanWebView.isCrosswalk()) {\n            if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED){\n                permissionsToRequest.add(Manifest.permission.READ_CONTACTS);\n            }\n            if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED){\n                permissionsToRequest.add(Manifest.permission.WRITE_CONTACTS);\n            }\n        }\n\n        if (permissionsToRequest.isEmpty()) {\n            handleSplash(config);\n        } else {\n            ActivityCompat.requestPermissions(this,\n                    permissionsToRequest.toArray(new String[]{}),\n                    REQUEST_STARTUP_PERMISSIONS);\n        }\n    }\n\n    @Override\n    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {\n        super.onRequestPermissionsResult(requestCode, permissions, grantResults);\n        startMainActivity();\n    }\n\n    private void handleSplash(AppConfig config){\n        // Check app if unlicensed and show banner\n        if (!config.isLicensed()) {\n            findViewById(R.id.banner_text).setVisibility(View.VISIBLE);\n        }\n\n        Handler handler = new Handler(Looper.getMainLooper());\n\n        // handle splash\n        double delay = 1.5; // in seconds\n        double forceTime = config.showSplashForceTime;\n        double maxTime = config.showSplashMaxTime;\n        if (forceTime > 0) {\n            delay = forceTime;\n        } else if (maxTime > 0) {\n            delay = maxTime;\n        }\n        handler.postDelayed(this::startMainActivity, (long)delay * 1000);\n    }\n\n    private void startMainActivity() {\n        Intent intent = new Intent(this, MainActivity.class);\n        // Make MainActivity think it was started from launcher\n        intent.addCategory(Intent.CATEGORY_LAUNCHER);\n        intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);\n        startActivity(intent);\n        finish();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/TabManager.java",
    "content": "package io.gonative.android;\n\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.text.TextUtils;\n\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\n\nimport com.aurelhubert.ahbottomnavigation.AHBottomNavigation;\nimport com.aurelhubert.ahbottomnavigation.AHBottomNavigationItem;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.LeanUtils;\nimport io.gonative.android.icons.Icon;\n\n/**\n * Created by Weiyin He on 9/22/14.\n * Copyright 2014 GoNative.io LLC\n */\npublic class TabManager implements AHBottomNavigation.OnTabSelectedListener {\n    private static final String TAG = TabManager.class.getName();\n    private MainActivity mainActivity;\n    private AHBottomNavigation bottomNavigationView;\n    private String currentMenuId;\n    private String currentUrl;\n    private JSONArray tabs;\n    private Map<String, TabMenu> tabMenus;\n    private final int maxTabs = 5;\n    private int tabbar_icon_size;\n    private int tabbar_icon_padding;\n    private Map<JSONObject, List<Pattern>> tabRegexCache = new HashMap<>(); // regex for each tab to auto-select\n    private boolean useJavascript; // do not use tabs from config\n    AppConfig appConfig;\n    private boolean performAction = true;\n\n\n    @SuppressWarnings(\"unused\")\n    private TabManager(){\n        // disable instantiation without mainActivity\n    }\n\n    TabManager(MainActivity mainActivity, AHBottomNavigation bottomNavigationView) {\n        this.mainActivity = mainActivity;\n        tabbar_icon_size = this.mainActivity.getResources().getInteger(R.integer.tabbar_icon_size);\n        tabbar_icon_padding = this.mainActivity.getResources().getInteger(R.integer.tabbar_icon_padding);\n        this.bottomNavigationView = bottomNavigationView;\n        this.bottomNavigationView.setOnTabSelectedListener(this);\n        this.appConfig = AppConfig.getInstance(this.mainActivity);\n\n        this.bottomNavigationView.setTitleState(AHBottomNavigation.TitleState.ALWAYS_SHOW);\n        this.bottomNavigationView.setDefaultBackgroundColor(mainActivity.getResources().getColor(R.color.tabBarBackground));\n        this.bottomNavigationView.setAccentColor(mainActivity.getResources().getColor(R.color.tabBarIndicator));\n        this.bottomNavigationView.setInactiveColor(mainActivity.getResources().getColor(R.color.tabBarTextColor));\n\n        this.bottomNavigationView.setTitleTextSizeInSp(12, 12);\n\n        BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (intent.getAction() != null && intent.getAction().equals(AppConfig.PROCESSED_TAB_NAVIGATION_MESSAGE)) {\n                    currentMenuId = null;\n                    initializeTabMenus();\n                    checkTabs(currentUrl);\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this.mainActivity)\n                .registerReceiver(broadcastReceiver,\n                        new IntentFilter(AppConfig.PROCESSED_TAB_NAVIGATION_MESSAGE));\n\n        initializeTabMenus();\n    }\n\n    private void initializeTabMenus(){\n        ArrayList<Pattern> regexes = appConfig.tabMenuRegexes;\n        ArrayList<String> ids = appConfig.tabMenuIDs;\n\n        if (regexes == null || ids == null) {\n            return;\n        }\n\n        tabMenus = new HashMap<>();\n        Map<String, Pattern> tabSelectionConfig = new HashMap<>();\n\n        for (int i = 0; i < ids.size(); i++) {\n            tabSelectionConfig.put(ids.get(i), regexes.get(i));\n        }\n\n        for (Map.Entry<String, JSONArray> tabMenu : appConfig.tabMenus.entrySet()) {\n            TabMenu item = new TabMenu();\n            item.tabs = tabMenu.getValue();\n            item.urlRegex = tabSelectionConfig.get(tabMenu.getKey());\n            tabMenus.put(tabMenu.getKey(), item);\n        }\n    }\n\n    public void checkTabs(String url) {\n        this.currentUrl = url;\n\n        if (this.mainActivity == null || url == null) {\n            return;\n        }\n\n        if (this.useJavascript) {\n            autoSelectTab(url);\n            return;\n        }\n\n        ArrayList<Pattern> regexes = appConfig.tabMenuRegexes;\n        ArrayList<String> ids = appConfig.tabMenuIDs;\n        if (regexes == null || ids == null) {\n            hideTabs();\n            return;\n        }\n\n        String menuId = null;\n\n        for (int i = 0; i < regexes.size(); i++) {\n            Pattern regex = regexes.get(i);\n            if (regex.matcher(url).matches()) {\n                menuId = ids.get(i);\n                break;\n            }\n        }\n\n        setMenuID(menuId);\n\n        if (menuId != null) autoSelectTab(url);\n    }\n\n\n\n    private void setMenuID(String id){\n        if (id == null) {\n            this.currentMenuId = null;\n            hideTabs();\n        }\n        else if (this.currentMenuId == null || !this.currentMenuId.equals(id)) {\n            this.currentMenuId = id;\n            JSONArray tabs = AppConfig.getInstance(this.mainActivity).tabMenus.get(id);\n            setTabs(tabs);\n            if(bottomNavigationView.getItemsCount() == 0) {\n                hideTabs();\n            } else {\n                showTabs();\n            }\n        }\n    }\n\n    private void setTabs(JSONArray tabs) {\n        this.tabs = tabs;\n\n        int selectedNumber = -1;\n        bottomNavigationView.removeAllItems();\n        if(tabs == null) return;\n    \n        for (int i = 0; i < tabs.length(); i++) {\n            if(i > (maxTabs-1)){\n                GNLog.getInstance().logError(TAG, \"Tab menu items list should not have more than 5 items\");\n                break;\n            }\n\n            JSONObject item = tabs.optJSONObject(i);\n            if (item == null) continue;\n\n            String label = item.optString(\"label\");\n            String icon = item.optString(\"icon\");\n\n            // if no label, icon and url is provided, do not include\n            if(label.isEmpty() && icon.isEmpty() && item.optString(\"url\").isEmpty()){\n                continue;\n            }\n\n            // set default drawable \"Question Mark\" when no icon provided\n            if (icon.isEmpty()) {\n                icon = \"faw_question\";\n                GNLog.getInstance().logError(TAG, \"All tabs must have icons.\");\n            }\n            \n            AHBottomNavigationItem navigationItem = new AHBottomNavigationItem(label, new Icon(mainActivity.getApplicationContext(), icon, tabbar_icon_size, mainActivity.getResources().getColor(R.color.tabBarTextColor)).getDrawable());\n            bottomNavigationView.addItem(navigationItem);\n\n            if (item.optBoolean(\"selected\")) {\n                selectedNumber = i;\n            }\n        }\n\n        if (selectedNumber > -1) {\n            selectTabNumber(selectedNumber, true);\n        }\n    }\n\n    private void showTabs() {\n        this.mainActivity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                mainActivity.showTabs();\n            }\n        });\n    }\n\n    private void hideTabs() {\n        this.mainActivity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                mainActivity.hideTabs();\n            }\n        });\n    }\n\n    // regex used for auto tab selection\n    private List<Pattern> getRegexForTab(JSONObject tabConfig) {\n        if (tabConfig == null) return null;\n\n        Object regex = tabConfig.opt(\"regex\");\n        if (regex == null) return null;\n\n        return LeanUtils.createRegexArrayFromStrings(regex);\n    }\n\n    private List<Pattern> getCachedRegexForTab(int position) {\n        if (tabs == null || position < 0 || position >= tabs.length()) return null;\n\n        JSONObject tabConfig = tabs.optJSONObject(position);\n        if (tabConfig == null) return null;\n\n        if (tabRegexCache.containsKey(tabConfig)) {\n            return tabRegexCache.get(tabConfig);\n        } else {\n            List<Pattern> regex = getRegexForTab(tabConfig);\n            tabRegexCache.put(tabConfig, regex);\n            return regex;\n        }\n    }\n\n    public void autoSelectTab(String url) {\n        if (tabs == null) return;\n\n        for (int i = 0; i < tabs.length(); i++) {\n            List<Pattern> patternList = getCachedRegexForTab(i);\n            if (patternList == null) continue;\n\n            for(Pattern regex : patternList) {\n                if (regex.matcher(url).matches()) {\n                    bottomNavigationView.setCurrentItem(i, false);\n                    return;\n                }\n            }\n        }\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public boolean selectTab(String url, String javascript) {\n        if (url == null) return false;\n\n        if (javascript == null) javascript = \"\";\n\n        if (this.tabs != null) {\n            for (int i = 0; i < this.tabs.length(); i++) {\n                JSONObject entry = this.tabs.optJSONObject(i);\n                if (entry != null) {\n                    String entryUrl = entry.optString(\"url\");\n                    String entryJs = entry.optString(\"javascript\");\n\n                    if (entryUrl == null) continue;\n                    if (entryJs == null) entryJs = \"\";\n\n                    if (url.equals(entryUrl) && javascript.equals(entryJs)) {\n                        if (this.bottomNavigationView != null) {\n                            this.bottomNavigationView.setCurrentItem(i, false);\n                            return true;\n                        }\n                    }\n\n                }\n            }\n        }\n\n        return false;\n    }\n\n    public void setTabsWithJson(JSONObject tabsJson, int tabMenuId) {\n        if(tabsJson == null) return;\n\n        this.useJavascript = true;\n\n        JSONArray tabs = tabsJson.optJSONArray(\"items\");\n        if (tabs != null) setTabs(tabs);\n\n        if(tabMenuId != -1){\n            TabMenu tabMenu = tabMenus.get(Integer.toString(tabMenuId));\n            if(tabMenu == null || tabs != null) return;\n            setTabs(tabMenu.tabs);\n        }\n\n        Object enabled = tabsJson.opt(\"enabled\");\n        if (enabled instanceof Boolean) {\n            if ((Boolean)enabled) {\n                this.showTabs();\n            } else {\n                this.hideTabs();\n            }\n        }\n    }\n\n    public void selectTabNumber(int tabNumber, boolean performAction) {\n        if (tabNumber < 0 || tabNumber >= bottomNavigationView.getItemsCount()) {\n            return;\n        }\n        this.performAction = performAction;\n        this.bottomNavigationView.setCurrentItem(tabNumber);\n    }\n\n    @Override\n    public boolean onTabSelected(int position, boolean wasSelected) {\n        if (this.tabs != null && position < this.tabs.length() && position != -1) {\n            JSONObject entry = this.tabs.optJSONObject(position);\n\n            String url = entry.optString(\"url\");\n            String javascript = entry.optString(\"javascript\");\n\n            if (!performAction) {\n                performAction = true;\n                return true;\n            }\n\n            if (!TextUtils.isEmpty(url)) {\n                if (!TextUtils.isEmpty(javascript)) mainActivity.loadUrlAndJavascript(url, javascript, true);\n                else mainActivity.loadUrl(url, true);\n            }\n        }\n        return true;\n    }\n\n    private class TabMenu {\n        Pattern urlRegex;\n        JSONArray tabs;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/UrlInspector.java",
    "content": "package io.gonative.android;\n\nimport android.content.Context;\n\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.regex.PatternSyntaxException;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * Created by weiyin on 4/28/14.\n */\npublic class UrlInspector {\n    private static final String TAG = UrlInspector.class.getName();\n    // singleton\n    private static UrlInspector instance = null;\n\n    private Pattern userIdRegex = null;\n\n    private String userId = null;\n\n\n    public static UrlInspector getInstance(){\n        if (instance == null) {\n            instance = new UrlInspector();\n        }\n        return instance;\n    }\n\n    public void init(Context context) {\n        String regexString = AppConfig.getInstance(context).userIdRegex;\n        if (regexString != null && !regexString.isEmpty()) {\n            try {\n                userIdRegex = Pattern.compile(regexString);\n            } catch (PatternSyntaxException e) {\n                GNLog.getInstance().logError(TAG, e.getMessage(), e);\n            }\n        }\n    }\n\n    private UrlInspector() {\n        // prevent direct instantiation\n    }\n\n    public void inspectUrl(String url) {\n        if (userIdRegex != null) {\n            Matcher matcher = userIdRegex.matcher(url);\n            if (matcher.groupCount() > 0 && matcher.find()) {\n                setUserId(matcher.group(1));\n            }\n        }\n    }\n\n    public String getUserId() {\n        return userId;\n    }\n\n    private void setUserId(String userId) {\n        this.userId = userId;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/UrlNavigation.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.SuppressLint;\nimport android.app.Activity;\nimport android.content.ActivityNotFoundException;\nimport android.content.Intent;\nimport android.content.SharedPreferences;\nimport android.location.LocationManager;\nimport android.net.Uri;\nimport android.net.http.SslError;\nimport android.os.AsyncTask;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.os.Message;\nimport android.preference.PreferenceManager;\nimport android.provider.Settings;\nimport android.security.KeyChain;\nimport android.security.KeyChainAliasCallback;\nimport android.text.TextUtils;\nimport android.util.Log;\nimport android.util.Pair;\nimport android.webkit.ClientCertRequest;\nimport android.webkit.CookieManager;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\nimport android.widget.Toast;\n\nimport androidx.annotation.RequiresApi;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.io.BufferedInputStream;\nimport java.io.ByteArrayOutputStream;\nimport java.io.InputStream;\nimport java.net.URISyntaxException;\nimport java.security.PrivateKey;\nimport java.security.cert.X509Certificate;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.regex.Pattern;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\nimport io.gonative.gonative_core.IOUtils;\nimport io.gonative.gonative_core.LeanUtils;\nimport io.gonative.gonative_core.Utils;\n\nenum WebviewLoadState {\n    STATE_UNKNOWN,\n    STATE_START_LOAD, // we have decided to load the url in this webview in shouldOverrideUrlLoading\n    STATE_PAGE_STARTED, // onPageStarted has been called\n    STATE_DONE // onPageFinished has been called\n}\n\npublic class UrlNavigation {\n    public static final String STARTED_LOADING_MESSAGE = \"io.gonative.android.webview.started\";\n    public static final String FINISHED_LOADING_MESSAGE = \"io.gonative.android.webview.finished\";\n    public static final String CLEAR_POOLS_MESSAGE = \"io.gonative.android.webview.clearPools\";\n\n    private static final String TAG = UrlNavigation.class.getName();\n\n    private static final String ASSET_URL = \"file:///android_asset/\";\n    public static final String OFFLINE_PAGE_URL = \"file:///android_asset/offline.html\";\n    public static final String OFFLINE_PAGE_URL_RAW = \"file:///offline.html\";\n\n    public static final int DEFAULT_HTML_SIZE = 10 * 1024; // 10 kilobytes\n\n    private MainActivity mainActivity;\n    private String profilePickerExec;\n    private String currentWebviewUrl;\n    private String JSBridgeScript;\n    private HtmlIntercept htmlIntercept;\n    private Handler startLoadTimeout = new Handler();\n\n    private WebviewLoadState state = WebviewLoadState.STATE_UNKNOWN;\n    private boolean mVisitedLoginOrSignup = false;\n    private boolean finishOnExternalUrl = false;\n    private double connectionOfflineTime;\n\n    private String interceptedRedirectUrl = \"\";\n\n    UrlNavigation(MainActivity activity) {\n        this.mainActivity = activity;\n        this.htmlIntercept = new HtmlIntercept(activity);\n\n        AppConfig appConfig = AppConfig.getInstance(mainActivity);\n\n        // profile picker\n        if (appConfig.profilePickerJS != null) {\n            this.profilePickerExec = \"gonative_profile_picker.parseJson(eval(\"\n                    + LeanUtils.jsWrapString(appConfig.profilePickerJS)\n                    + \"))\";\n        }\n\n        if (mainActivity.getIntent().getBooleanExtra(MainActivity.EXTRA_WEBVIEW_WINDOW_OPEN, false)) {\n            finishOnExternalUrl = true;\n        }\n\n        connectionOfflineTime = appConfig.androidConnectionOfflineTime;\n\t}\n\n    private boolean isInternalUri(Uri uri) {\n        String scheme = uri.getScheme();\n        if (scheme == null || (!scheme.equalsIgnoreCase(\"http\") && !scheme.equalsIgnoreCase(\"https\"))) {\n            return false;\n        }\n\n        AppConfig appConfig = AppConfig.getInstance(mainActivity);\n        String urlString = uri.toString();\n\n        // first check regexes\n        ArrayList<Pattern> regexes = appConfig.regexInternalExternal;\n        ArrayList<Boolean> isInternal = appConfig.regexIsInternal;\n        if (regexes != null) {\n            for (int i = 0; i < regexes.size(); i++) {\n                Pattern regex = regexes.get(i);\n                if (regex.matcher(urlString).matches()) {\n                    return isInternal.get(i);\n                }\n            }\n        }\n\n        String host = uri.getHost();\n        String initialHost = appConfig.initialHost;\n\n        return host != null &&\n                (host.equals(initialHost) || host.endsWith(\".\" + initialHost));\n    }\n\n    public boolean shouldOverrideUrlLoading(GoNativeWebviewInterface view, String url) {\n        return shouldOverrideUrlLoading(view, url, false, false);\n    }\n\n    // noAction to skip stuff like opening url in external browser, higher nav levels, etc.\n    private boolean shouldOverrideUrlLoadingNoIntercept(final GoNativeWebviewInterface view, final String url,\n                                                        @SuppressWarnings(\"SameParameterValue\") final boolean noAction) {\n//\t\tLog.d(TAG, \"shouldOverrideUrl: \" + url);\n\n        // return if url is null (can happen if clicking refresh when there is no page loaded)\n        if (url == null)\n            return false;\n\n        // return if loading from local assets\n        if (url.startsWith(ASSET_URL)) return false;\n\n        if (url.startsWith(\"blob:\")) return false;\n\n        view.setCheckLoginSignup(true);\n\n        Uri uri = Uri.parse(url);\n\n        if (uri.getScheme() != null && uri.getScheme().equals(\"gonative-bridge\")) {\n            if (noAction) return true;\n\n            try {\n                String json = uri.getQueryParameter(\"json\");\n\n                JSONArray parsedJson = new JSONArray(json);\n                for (int i = 0; i < parsedJson.length(); i++) {\n                    JSONObject entry = parsedJson.optJSONObject(i);\n                    if (entry == null) continue;\n\n                    String command = entry.optString(\"command\");\n                    if (command.isEmpty()) continue;\n\n                    if (command.equals(\"pop\")) {\n                        if (mainActivity.isNotRoot()) mainActivity.finish();\n                    } else if (command.equals(\"clearPools\")) {\n                        LocalBroadcastManager.getInstance(mainActivity).sendBroadcast(\n                                new Intent(UrlNavigation.CLEAR_POOLS_MESSAGE));\n                    }\n                }\n            } catch (Exception e) {\n                // do nothing\n            }\n\n            return true;\n        }\n\n        final AppConfig appConfig = AppConfig.getInstance(mainActivity);\n\n        // Check native bridge urls\n        if (\"gonative\".equals(uri.getScheme()) && currentWebviewUrl != null &&\n                !LeanUtils.checkNativeBridgeUrls(currentWebviewUrl, mainActivity)) {\n            GNLog.getInstance().logError(TAG, \"URL not authorized for native bridge: \" + currentWebviewUrl);\n            return true;\n        }\n\n        if (\"gonative\".equals(uri.getScheme())) {\n            ((GoNativeApplication) mainActivity.getApplication()).mBridge.handleJSBridgeFunctions(mainActivity, uri);\n            return true;\n        }\n\n        // check redirects\n        if (appConfig.getRedirects() != null) {\n            String to = appConfig.getRedirects().get(url);\n            if (to == null) to = appConfig.getRedirects().get(\"*\");\n            if (to != null && !to.equals(url)) {\n                if (noAction) return true;\n\n                final String destination = to;\n                mainActivity.runOnUiThread(new Runnable() {\n                    @Override\n                    public void run() {\n                        mainActivity.loadUrl(destination);\n                    }\n                });\n                return true;\n            }\n        }\n\n        if (!isInternalUri(uri)) {\n            if (noAction) return true;\n\n            Log.d(TAG, \"processing dynamic link: \" + uri);\n            Intent intent = null;\n            // launch browser\n            try {\n                if (uri.getScheme().equals(\"intent\")) {\n                    intent = Intent.parseUri(uri.toString(), Intent.URI_INTENT_SCHEME);\n                } else {\n                    intent = new Intent(Intent.ACTION_VIEW, uri);\n                }\n                mainActivity.startActivity(intent);\n            } catch (ActivityNotFoundException ex) {\n                // Try loading fallback url if available\n                if (intent != null) {\n                    String fallbackUrl = intent.getStringExtra(\"browser_fallback_url\");\n                    if (!TextUtils.isEmpty(fallbackUrl)) {\n                        mainActivity.loadUrl(fallbackUrl);\n                    } else {\n                        Toast.makeText(mainActivity, R.string.app_not_installed, Toast.LENGTH_LONG).show();\n                        GNLog.getInstance().logError(TAG, mainActivity.getString(R.string.app_not_installed), ex, GNLog.TYPE_TOAST_ERROR);\n                    }\n                }\n            } catch (URISyntaxException e) {\n                GNLog.getInstance().logError(TAG, e.getMessage(), e);\n            }\n            return true;\n        }\n\n        // Starting here, we are going to load the request, but possibly in a\n        // different activity depending on the structured nav level\n\n        if (!mainActivity.isRestoreBrightnessOnNavigation()) {\n            mainActivity.setBrightness(-1);\n            mainActivity.setRestoreBrightnessOnNavigation(false);\n        }\n\n        if (appConfig.maxWindowsEnabled) {\n\n            GoNativeWindowManager windowManager = mainActivity.getGNWindowManager();\n\n            // To prevent consecutive calls and handle MaxWindows correctly\n            // Checks for a flag indicating if the Activity was created from CreateNewWindow OR NavLevels\n            // and avoid triggering MaxWindows during this initial intercept\n            boolean ignoreInterceptMaxWindows = windowManager.isIgnoreInterceptMaxWindows(mainActivity.getActivityId());\n\n            if (ignoreInterceptMaxWindows) {\n                windowManager.setIgnoreInterceptMaxWindows(mainActivity.getActivityId(), false);\n            } else if (appConfig.numWindows > 0 && windowManager.getWindowCount() >= appConfig.numWindows) {\n                if (mainActivity.onMaxWindowsReached(url)) {\n                    return true;\n                }\n            }\n        }\n\n        int currentLevel = mainActivity.getUrlLevel();\n        int newLevel = mainActivity.urlLevelForUrl(url);\n        if (currentLevel >= 0 && newLevel >= 0) {\n            if (newLevel > currentLevel) {\n                if (noAction) return true;\n\n                // new activity\n                Intent intent = new Intent(mainActivity.getBaseContext(), MainActivity.class);\n                intent.putExtra(\"isRoot\", false);\n                intent.putExtra(\"url\", url);\n                intent.putExtra(\"parentUrlLevel\", currentLevel);\n                intent.putExtra(\"postLoadJavascript\", mainActivity.postLoadJavascript);\n\n                if (appConfig.maxWindowsEnabled) {\n                    intent.putExtra(MainActivity.EXTRA_IGNORE_INTERCEPT_MAXWINDOWS, true);\n                }\n\n                mainActivity.startActivityForResult(intent, MainActivity.REQUEST_WEB_ACTIVITY);\n\n                mainActivity.postLoadJavascript = null;\n                mainActivity.postLoadJavascriptForRefresh = null;\n\n                return true;\n            } else if (newLevel < currentLevel && newLevel <= mainActivity.getParentUrlLevel()) {\n                if (noAction) return true;\n\n                // pop activity\n                Intent returnIntent = new Intent();\n                returnIntent.putExtra(\"url\", url);\n                returnIntent.putExtra(\"urlLevel\", newLevel);\n                returnIntent.putExtra(\"postLoadJavascript\", mainActivity.postLoadJavascript);\n                mainActivity.setResult(Activity.RESULT_OK, returnIntent);\n                mainActivity.finish();\n                return true;\n            }\n        }\n\n        // Starting here, the request will be loaded in this activity.\n        if (newLevel >= 0) {\n            mainActivity.setUrlLevel(newLevel);\n        }\n\n        final String newTitle = mainActivity.titleForUrl(url);\n        if (newTitle != null) {\n            if (!noAction) {\n                mainActivity.runOnUiThread(new Runnable() {\n                    @Override\n                    public void run() {\n                        mainActivity.setTitle(newTitle);\n                    }\n                });\n            }\n        }\n\n        // nav title image\n        if (!noAction) {\n            mainActivity.runOnUiThread(() ->\n                    mainActivity.getActionManager().setupTitleDisplayForUrl(url)\n            );\n        }\n\n        // check to see if the webview exists in pool.\n        WebViewPool webViewPool = ((GoNativeApplication) mainActivity.getApplication()).getWebViewPool();\n        Pair<GoNativeWebviewInterface, WebViewPoolDisownPolicy> pair = webViewPool.webviewForUrl(url);\n        final GoNativeWebviewInterface poolWebview = pair.first;\n        WebViewPoolDisownPolicy poolDisownPolicy = pair.second;\n\n        if (noAction && poolWebview != null) return true;\n\n        if (poolWebview != null && poolDisownPolicy == WebViewPoolDisownPolicy.Always) {\n            this.mainActivity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    mainActivity.switchToWebview(poolWebview, true, false);\n                    mainActivity.checkNavigationForPage(url);\n                }\n            });\n            webViewPool.disownWebview(poolWebview);\n            LocalBroadcastManager.getInstance(mainActivity).sendBroadcast(new Intent(UrlNavigation.FINISHED_LOADING_MESSAGE));\n            return true;\n        }\n\n        if (poolWebview != null && poolDisownPolicy == WebViewPoolDisownPolicy.Never) {\n            this.mainActivity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    mainActivity.switchToWebview(poolWebview, true, false);\n                    mainActivity.checkNavigationForPage(url);\n                }\n            });\n            return true;\n        }\n\n        if (poolWebview != null && poolDisownPolicy == WebViewPoolDisownPolicy.Reload &&\n                !LeanUtils.urlsMatchOnPath(url, this.currentWebviewUrl)) {\n            this.mainActivity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    mainActivity.switchToWebview(poolWebview, true, false);\n                    mainActivity.checkNavigationForPage(url);\n                }\n            });\n            return true;\n        }\n\n        if (this.mainActivity.isPoolWebview) {\n            // if we are here, either the policy is reload and we are reloading the page, or policy is never but we are going to a different page. So take ownership of the webview.\n            webViewPool.disownWebview(view);\n            this.mainActivity.isPoolWebview = false;\n        }\n\n        return false;\n    }\n\n    public boolean shouldOverrideUrlLoading(final GoNativeWebviewInterface view, String url,\n                                            @SuppressWarnings(\"unused\") boolean isReload, boolean isRedirect) {\n        if (url == null) return false;\n\n        boolean shouldOverride = shouldOverrideUrlLoadingNoIntercept(view, url, false);\n        if (shouldOverride) {\n            if (finishOnExternalUrl) {\n                mainActivity.finish();\n            }\n\n            // Check if intercepted URL request was a result of a server-side redirect.\n            // Redirect URLs triggers redundant onPageFinished()\n            if (isRedirect) {\n                interceptedRedirectUrl = url;\n            }\n            return true;\n        } else {\n            finishOnExternalUrl = false;\n        }\n\n        // intercept html\n        this.htmlIntercept.setInterceptUrl(url);\n        mainActivity.hideWebview();\n        state = WebviewLoadState.STATE_START_LOAD;\n        // 10 second (default) delay to get to onPageStarted or doUpdateVisitedHistory\n        if (!Double.isNaN(connectionOfflineTime) && !Double.isInfinite(connectionOfflineTime) &&\n                connectionOfflineTime > 0) {\n            startLoadTimeout.postDelayed(new Runnable() {\n                @Override\n                public void run() {\n                    AppConfig appConfig = AppConfig.getInstance(mainActivity);\n                    String url = view.getUrl();\n                    if (appConfig.showOfflinePage && !OFFLINE_PAGE_URL.equals(url)) {\n                        view.loadUrlDirect(OFFLINE_PAGE_URL);\n                    }\n                }\n            }, (long) (connectionOfflineTime * 1000));\n        }\n\n        return false;\n    }\n\n    public void onPageStarted(String url) {\n        // catch blank pages from htmlIntercept and cancel loading\n        if (url.equals(htmlIntercept.getRedirectedUrl())) {\n            mainActivity.goBack();\n            htmlIntercept.setRedirectedUrl(null);\n            return;\n        }\n\n        state = WebviewLoadState.STATE_PAGE_STARTED;\n        startLoadTimeout.removeCallbacksAndMessages(null);\n        htmlIntercept.setInterceptUrl(url);\n\n        UrlInspector.getInstance().inspectUrl(url);\n        Uri uri = Uri.parse(url);\n\n        // reload menu if internal url\n        if (AppConfig.getInstance(mainActivity).loginDetectionUrl != null && isInternalUri(uri)) {\n            mainActivity.updateMenu();\n        }\n\n        // check ready status\n        mainActivity.startCheckingReadyStatus();\n\n        mainActivity.checkPreNavigationForPage(url);\n\n        // send broadcast message\n        LocalBroadcastManager.getInstance(mainActivity).sendBroadcast(new Intent(UrlNavigation.STARTED_LOADING_MESSAGE));\n\n\n        // enable swipe refresh controller if offline page\n        if (OFFLINE_PAGE_URL.equals(url)) {\n            mainActivity.enableSwipeRefresh();\n        } else {\n            mainActivity.restoreSwipRefreshDefault();\n        }\n    }\n\n    @SuppressWarnings(\"unused\")\n    public void showWebViewImmediately() {\n        mainActivity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                mainActivity.showWebviewImmediately();\n            }\n        });\n    }\n\n    @SuppressLint(\"ApplySharedPref\")\n    public void onPageFinished(GoNativeWebviewInterface view, String url) {\n        // Catch intercepted Redirect URL to\n        // prevent loading unnecessary components\n        if (interceptedRedirectUrl.equals(url)) {\n            interceptedRedirectUrl = \"\";\n            return;\n        }\n\n        Log.d(TAG, \"onpagefinished \" + url);\n        state = WebviewLoadState.STATE_DONE;\n        setCurrentWebviewUrl(url);\n\n        AppConfig appConfig = AppConfig.getInstance(mainActivity);\n        if (url != null && appConfig.ignorePageFinishedRegexes != null) {\n            for (Pattern pattern : appConfig.ignorePageFinishedRegexes) {\n                if (pattern.matcher(url).matches()) return;\n            }\n        }\n\n        mainActivity.runOnUiThread(new Runnable() {\n            @Override\n            public void run() {\n                mainActivity.showWebview();\n            }\n        });\n\n        UrlInspector.getInstance().inspectUrl(url);\n\n        Uri uri = Uri.parse(url);\n        if (isInternalUri(uri)) {\n            AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {\n                @Override\n                public void run() {\n                    CookieManager.getInstance().flush();\n                }\n            });\n        }\n\n        if (appConfig.loginDetectionUrl != null) {\n            if (mVisitedLoginOrSignup) {\n                mainActivity.updateMenu();\n            }\n\n            mVisitedLoginOrSignup = LeanUtils.urlsMatchOnPath(url, appConfig.loginUrl) ||\n                    LeanUtils.urlsMatchOnPath(url, appConfig.signupUrl);\n        }\n\n        // post-load javascript\n        if (appConfig.postLoadJavascript != null) {\n            view.runJavascript(appConfig.postLoadJavascript);\n        }\n\n        // profile picker\n        if (this.profilePickerExec != null) {\n            view.runJavascript(this.profilePickerExec);\n        }\n\n        // tabs\n        mainActivity.checkNavigationForPage(url);\n\n        // post-load javascript\n        if (mainActivity.postLoadJavascript != null) {\n            String js = mainActivity.postLoadJavascript;\n            mainActivity.postLoadJavascript = null;\n            mainActivity.runJavascript(js);\n        }\n\n        // send broadcast message\n        LocalBroadcastManager.getInstance(mainActivity).sendBroadcast(new Intent(UrlNavigation.FINISHED_LOADING_MESSAGE));\n\n        boolean doNativeBridge = true;\n        if (currentWebviewUrl != null) {\n            doNativeBridge = LeanUtils.checkNativeBridgeUrls(currentWebviewUrl, mainActivity);\n        }\n\n        // send installation info\n        if (doNativeBridge) {\n            runGonativeDeviceInfo(\"gonative_device_info\");\n        }\n\n        injectJSBridgeLibrary(currentWebviewUrl);\n        ((GoNativeApplication) mainActivity.getApplication()).mBridge.onPageFinish(mainActivity, doNativeBridge);\n    }\n\n    private void injectJSBridgeLibrary(String currentWebviewUrl) {\n        if(!LeanUtils.checkNativeBridgeUrls(currentWebviewUrl, mainActivity)) return;\n\n        try {\n            if(JSBridgeScript == null) {\n                ByteArrayOutputStream baos = new ByteArrayOutputStream();\n                InputStream is = new BufferedInputStream(mainActivity.getAssets().open(\"GoNativeJSBridgeLibrary.js\"));\n                IOUtils.copy(is, baos);\n                JSBridgeScript = baos.toString();\n            }\n            mainActivity.runJavascript(JSBridgeScript);\n            ((GoNativeApplication) mainActivity.getApplication()).mBridge.injectJSLibraries(mainActivity);\n            // call the user created function that needs library access on page finished.\n            mainActivity.runJavascript(LeanUtils.createJsForCallback(\"gonative_library_ready\", null));\n        } catch (Exception e) {\n            Log.d(TAG, \"GoNative JSBridgeLibrary Injection Error:- \" + e.getMessage());\n        }\n    }\n\n    public void onFormResubmission(GoNativeWebviewInterface view, Message dontResend, Message resend) {\n        resend.sendToTarget();\n    }\n\n    private void runGonativeDeviceInfo(String callback) {\n        Map<String, Object> installationInfo = Installation.getInfo(mainActivity);\n        SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mainActivity);\n        if (!sharedPreferences.getBoolean(\"hasLaunched\", false)) {\n            sharedPreferences.edit().putBoolean(\"hasLaunched\", true).commit();\n            installationInfo.put(\"isFirstLaunch\", true);\n        } else {\n            installationInfo.put(\"isFirstLaunch\", false);\n        }\n\n        JSONObject jsonObject = new JSONObject(installationInfo);\n        String js = LeanUtils.createJsForCallback(callback, jsonObject);\n        mainActivity.runJavascript(js);\n    }\n\n    public void doUpdateVisitedHistory(@SuppressWarnings(\"unused\") GoNativeWebviewInterface view, String url, boolean isReload) {\n        if (state == WebviewLoadState.STATE_START_LOAD) {\n            state = WebviewLoadState.STATE_PAGE_STARTED;\n            startLoadTimeout.removeCallbacksAndMessages(null);\n        }\n\n        if (!isReload && !url.equals(OFFLINE_PAGE_URL)) {\n            mainActivity.addToHistory(url);\n        }\n    }\n\n    public void onReceivedError(final GoNativeWebviewInterface view,\n                                @SuppressWarnings(\"unused\") int errorCode,\n                                String errorDescription, String failingUrl) {\n        if (errorDescription != null && errorDescription.contains(\"net::ERR_CACHE_MISS\")) {\n            mainActivity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    view.reload();\n                }\n            });\n            return;\n        }\n\n        boolean showingOfflinePage = false;\n\n        // show offline page if not connected to internet\n        AppConfig appConfig = AppConfig.getInstance(this.mainActivity);\n        if (appConfig.showOfflinePage &&\n                (state == WebviewLoadState.STATE_PAGE_STARTED || state == WebviewLoadState.STATE_START_LOAD)) {\n\n            if (mainActivity.isDisconnected() ||\n                    (errorCode == WebViewClient.ERROR_HOST_LOOKUP &&\n                            failingUrl != null &&\n                            view.getUrl() != null &&\n                            failingUrl.equals(view.getUrl()))) {\n\n                showingOfflinePage = true;\n\n                mainActivity.runOnUiThread(new Runnable() {\n                    @Override\n                    public void run() {\n                        view.stopLoading();\n                        view.loadUrlDirect(OFFLINE_PAGE_URL);\n\n                        new Handler().postDelayed(new Runnable() {\n                            @Override\n                            public void run() {\n                                view.loadUrlDirect(OFFLINE_PAGE_URL);\n                            }\n                        }, 100);\n                    }\n                });\n            }\n        }\n\n        if (!showingOfflinePage) {\n            mainActivity.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    mainActivity.showWebview();\n                }\n            });\n        }\n    }\n\n    public void onReceivedSslError(SslError error, String webviewUrl) {\n        int errorMessage;\n        switch (error.getPrimaryError()) {\n            case SslError.SSL_EXPIRED:\n                errorMessage = R.string.ssl_error_expired;\n                break;\n            case SslError.SSL_DATE_INVALID:\n            case SslError.SSL_IDMISMATCH:\n            case SslError.SSL_NOTYETVALID:\n            case SslError.SSL_UNTRUSTED:\n                errorMessage = R.string.ssl_error_cert;\n                break;\n            case SslError.SSL_INVALID:\n            default:\n                errorMessage = R.string.ssl_error_generic;\n                break;\n        }\n\n        Toast.makeText(mainActivity, errorMessage, Toast.LENGTH_LONG).show();\n        String finalErrorMessage = mainActivity.getString(errorMessage) + \" - Error url: \" + error.getUrl() + \" - Source page: \" + webviewUrl;\n        GNLog.getInstance().logError(TAG, finalErrorMessage, new Exception(finalErrorMessage), GNLog.TYPE_TOAST_ERROR);\n    }\n\n    @SuppressWarnings(\"unused\")\n    public String getCurrentWebviewUrl() {\n        return currentWebviewUrl;\n    }\n\n    public void setCurrentWebviewUrl(String currentWebviewUrl) {\n        this.currentWebviewUrl = currentWebviewUrl;\n        ((GoNativeApplication) mainActivity.getApplication()).mBridge.setCurrentWebviewUrl(currentWebviewUrl);\n    }\n\n    public WebResourceResponse interceptHtml(LeanWebView view, String url) {\n//        Log.d(TAG, \"intercept \" + url);\n        return htmlIntercept.interceptHtml(view, url, this.currentWebviewUrl);\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    public boolean chooseFileUpload(String[] mimetypespec) {\n        return chooseFileUpload(mimetypespec, false);\n    }\n\n    public boolean chooseFileUpload(final String[] mimetypespec, final boolean multiple) {\n        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {\n            chooseFileUploadAfterPermission(mimetypespec, multiple);\n        } else {\n            List<String> permissionToRequest = new ArrayList<>();\n            permissionToRequest.add(Manifest.permission.READ_EXTERNAL_STORAGE);\n            permissionToRequest.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);\n            if (!Utils.isPermissionGranted(mainActivity, Manifest.permission.CAMERA)) {\n                permissionToRequest.add(Manifest.permission.CAMERA);\n            }\n            mainActivity.getPermission(permissionToRequest.toArray(new String[0]), (permissions, grantResults) -> chooseFileUploadAfterPermission(mimetypespec, multiple));\n        }\n        return true;\n    }\n\n    @SuppressWarnings(\"UnusedReturnValue\")\n    private boolean chooseFileUploadAfterPermission(String[] mimetypespec, boolean multiple) {\n        mainActivity.setDirectUploadImageUri(null);\n\n        FileUploadIntentsCreator creator = new FileUploadIntentsCreator(mainActivity, mimetypespec, multiple);\n\n        if (creator.videosAllowed() || creator.imagesAllowed()) {\n            mainActivity.getPermission(new String[]{Manifest.permission.CAMERA}, (permissions, grantResults) -> launchChooserIntent(creator));\n            return true;\n        }\n\n        return launchChooserIntent(creator);\n    }\n\n    private boolean launchChooserIntent(FileUploadIntentsCreator creator) {\n        Intent intentToSend = creator.chooserIntent();\n        mainActivity.setDirectUploadImageUri(creator.getCurrentCaptureUri());\n\n        try {\n            mainActivity.startActivityForResult(intentToSend, MainActivity.REQUEST_SELECT_FILE);\n            return true;\n        } catch (ActivityNotFoundException e) {\n            mainActivity.cancelFileUpload();\n            Toast.makeText(mainActivity, R.string.cannot_open_file_chooser, Toast.LENGTH_LONG).show();\n            return false;\n        }\n    }\n\n\n    public boolean openDirectCamera(final String[] mimetypespec, final boolean multiple) {\n        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {\n            if (!Utils.isPermissionGranted(mainActivity, Manifest.permission.CAMERA)) {\n                mainActivity.getPermission(new String[]{Manifest.permission.CAMERA}, (permissions, grantResults) -> openDirectCameraAfterPermission(mimetypespec, multiple));\n            } else {\n                openDirectCameraAfterPermission(mimetypespec, multiple);\n            }\n        } else {\n\n            ArrayList<String> permissionRequests = new ArrayList<>();\n            permissionRequests.add(Manifest.permission.READ_EXTERNAL_STORAGE);\n            permissionRequests.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);\n\n            if (!Utils.isPermissionGranted(mainActivity, Manifest.permission.CAMERA)) {\n                permissionRequests.add(Manifest.permission.CAMERA);\n            }\n\n            mainActivity.getPermission(permissionRequests.toArray(new String[0]), (permissions, grantResults) -> openDirectCameraAfterPermission(mimetypespec, multiple));\n        }\n        return true;\n    }\n\n    /*\n        Directly opens camera if the mime types are images. If not, run existing default process\n     */\n    @SuppressWarnings(\"UnusedReturnValue\")\n    private boolean openDirectCameraAfterPermission(String[] mimetypespec, boolean multiple) {\n\n        // Check and verify CAMERA permission so app will not crash when using cam\n        if (!Utils.isPermissionGranted(mainActivity, Manifest.permission.CAMERA)) {\n            Toast.makeText(mainActivity, R.string.upload_camera_permission_denied, Toast.LENGTH_SHORT).show();\n            return false;\n        }\n\n        mainActivity.setDirectUploadImageUri(null);\n\n        FileUploadIntentsCreator creator = new FileUploadIntentsCreator(mainActivity, mimetypespec, multiple);\n        Intent intentToSend = creator.cameraIntent();\n        mainActivity.setDirectUploadImageUri(creator.getCurrentCaptureUri());\n\n        try {\n            // Directly open the camera intent with the same Request Result value value\n            mainActivity.startActivityForResult(intentToSend, MainActivity.REQUEST_SELECT_FILE);\n            return true;\n        } catch (ActivityNotFoundException e) {\n            mainActivity.cancelFileUpload();\n            Toast.makeText(mainActivity, R.string.cannot_open_file_chooser, Toast.LENGTH_LONG).show();\n        }\n\n        return false;\n    }\n\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    public void createNewWindow(WebView webView, Message resultMsg) {\n        AppConfig appConfig = AppConfig.getInstance(mainActivity);\n        if (appConfig.maxWindowsEnabled && appConfig.numWindows > 0 && mainActivity.getGNWindowManager().getWindowCount() >= appConfig.numWindows) {\n            // All of these just to get new url\n            WebView newWebView = new WebView(webView.getContext());\n            WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;\n            transport.setWebView(newWebView);\n            resultMsg.sendToTarget();\n            newWebView.setWebViewClient(new WebViewClient() {\n                @Override\n                public void onPageFinished(WebView view, String url) {\n                    if (!mainActivity.onMaxWindowsReached(url)) {\n                        Intent intent = new Intent(mainActivity.getBaseContext(), MainActivity.class);\n                        intent.putExtra(\"isRoot\", false);\n                        intent.putExtra(\"url\", url);\n                        intent.putExtra(MainActivity.EXTRA_IGNORE_INTERCEPT_MAXWINDOWS, true);\n                        mainActivity.startActivityForResult(intent, MainActivity.REQUEST_WEB_ACTIVITY);\n                    }\n                }\n            });\n            return;\n        }\n        createNewWindow(resultMsg, appConfig.maxWindowsEnabled);\n    }\n\n    private void createNewWindow(Message resultMsg, boolean maxWindowsEnabled) {\n        ((GoNativeApplication) mainActivity.getApplication()).setWebviewMessage(resultMsg);\n        Intent intent = new Intent(mainActivity.getBaseContext(), MainActivity.class);\n        intent.putExtra(\"isRoot\", false);\n        intent.putExtra(MainActivity.EXTRA_WEBVIEW_WINDOW_OPEN, true);\n\n        if (maxWindowsEnabled) {\n            intent.putExtra(MainActivity.EXTRA_IGNORE_INTERCEPT_MAXWINDOWS, true);\n        }\n\n        // need to use startActivityForResult instead of startActivity because of singleTop launch mode\n        mainActivity.startActivityForResult(intent, MainActivity.REQUEST_WEB_ACTIVITY);\n    }\n\n    public boolean isLocationServiceEnabled() {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {\n            LocationManager lm = mainActivity.getSystemService(LocationManager.class);\n            return lm.isLocationEnabled();\n        } else {\n            // This is Deprecated in API 28\n            int mode = Settings.Secure.getInt(mainActivity.getContentResolver(), Settings.Secure.LOCATION_MODE,\n                    Settings.Secure.LOCATION_MODE_OFF);\n            return (mode != Settings.Secure.LOCATION_MODE_OFF);\n        }\n    }\n\n    protected void onDownloadStart() {\n        startLoadTimeout.removeCallbacksAndMessages(null);\n        state = WebviewLoadState.STATE_DONE;\n    }\n\n\n    private static class GetKeyTask extends AsyncTask<String, Void, Pair<PrivateKey, X509Certificate[]>> {\n        private Activity activity;\n        private ClientCertRequest request;\n\n        public GetKeyTask(Activity activity, ClientCertRequest request) {\n            this.activity = activity;\n            this.request = request;\n        }\n\n        @Override\n        protected Pair<PrivateKey, X509Certificate[]> doInBackground(String... strings) {\n            String alias = strings[0];\n\n            try {\n                PrivateKey privateKey = KeyChain.getPrivateKey(activity, alias);\n                X509Certificate[] certificates = KeyChain.getCertificateChain(activity, alias);\n                return new Pair<>(privateKey, certificates);\n            } catch (Exception e) {\n                GNLog.getInstance().logError(TAG, \"Error getting private key for alias \" + alias, e);\n                return null;\n            }\n        }\n\n        @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n        @Override\n        protected void onPostExecute(Pair<PrivateKey, X509Certificate[]> result) {\n            if (result != null && result.first != null & result.second != null) {\n                request.proceed(result.first, result.second);\n            } else {\n                request.ignore();\n            }\n        }\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    public void onReceivedClientCertRequest(String url, ClientCertRequest request) {\n        Uri uri = Uri.parse(url);\n        KeyChainAliasCallback callback = alias -> {\n            if (alias == null) {\n                request.ignore();\n                return;\n            }\n\n            new GetKeyTask(mainActivity, request).execute(alias);\n        };\n\n        KeyChain.choosePrivateKeyAlias(mainActivity, callback, request.getKeyTypes(), request.getPrincipals(), request.getHost(),\n                request.getPort(), null);\n    }\n\n    // Cancels scheduled display of offline page after timeout\n    public void cancelLoadTimeout() {\n        if (startLoadTimeout == null && state != WebviewLoadState.STATE_START_LOAD) return;\n        startLoadTimeout.removeCallbacksAndMessages(null);\n        showWebViewImmediately();\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/WebViewPool.java",
    "content": "package io.gonative.android;\n\nimport android.app.Activity;\nimport android.content.BroadcastReceiver;\nimport android.content.Context;\nimport android.content.Intent;\nimport android.content.IntentFilter;\nimport android.graphics.Point;\nimport androidx.localbroadcastmanager.content.LocalBroadcastManager;\nimport android.util.Pair;\nimport android.view.Display;\nimport android.view.WindowManager;\nimport android.webkit.WebResourceResponse;\n\nimport org.json.JSONArray;\nimport org.json.JSONObject;\n\nimport java.util.ArrayList;\nimport java.util.HashMap;\nimport java.util.HashSet;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Created by Weiyin He on 9/3/14.\n * Copyright 2014 GoNative.io LLC\n */\n\npublic class WebViewPool {\n    public class WebViewPoolCallback {\n        @SuppressWarnings(\"unused\")\n        public void onPageFinished(final GoNativeWebviewInterface webview, String url) {\n            WebViewPool pool = WebViewPool.this;\n\n            pool.urlToWebview.put(pool.currentLoadingUrl, pool.currentLoadingWebview);\n            pool.currentLoadingUrl = null;\n            pool.currentLoadingWebview = null;\n            pool.isLoading = false;\n            pool.htmlIntercept.setInterceptUrl(null);\n\n            pool.resumeLoading();\n        }\n\n        public WebResourceResponse interceptHtml(GoNativeWebviewInterface webview, String url) {\n            return htmlIntercept.interceptHtml(webview, url, null);\n        }\n    }\n\n    private Activity context;\n    private HtmlIntercept htmlIntercept;\n\n    private boolean isInitialized;\n    private Map<String, GoNativeWebviewInterface> urlToWebview;\n    private Map<String, WebViewPoolDisownPolicy> urlToDisownPolicy;\n\n    private WebViewPoolCallback webViewPoolCallback = new WebViewPoolCallback();\n\n    private List<Set<String>> urlSets;\n    private Set<String> urlsToLoad;\n    private GoNativeWebviewInterface currentLoadingWebview;\n    private String currentLoadingUrl;\n    private boolean isLoading;\n    private String lastUrlRequest;\n    private boolean isMainActivityLoading;\n\n    public void init(Activity activity) {\n        if (this.isInitialized) return;\n        this.isInitialized = true;\n\n        // webviews must be instantiated from activity context\n        this.context = activity;\n        this.htmlIntercept = new HtmlIntercept(activity);\n\n        this.urlToWebview = new HashMap<>();\n        this.urlToDisownPolicy = new HashMap<>();\n        this.urlSets = new ArrayList<>();\n        this.urlsToLoad = new HashSet<>();\n\n        // register for broadcast messages\n        BroadcastReceiver messageReceiver = new BroadcastReceiver() {\n            @Override\n            public void onReceive(Context context, Intent intent) {\n                if (intent == null || intent.getAction() == null) return;\n\n                switch (intent.getAction()) {\n                    case UrlNavigation.STARTED_LOADING_MESSAGE: {\n                        WebViewPool pool = WebViewPool.this;\n                        pool.isMainActivityLoading = true;\n                        if (pool.currentLoadingWebview != null) {\n                            // onReceive is always called on the main thread, so this is safe.\n                            pool.currentLoadingWebview.stopLoading();\n                            pool.isLoading = false;\n                        }\n                        break;\n                    }\n                    case UrlNavigation.FINISHED_LOADING_MESSAGE: {\n                        WebViewPool pool = WebViewPool.this;\n                        pool.isMainActivityLoading = false;\n                        pool.resumeLoading();\n                        break;\n                    }\n                    case AppConfig.PROCESSED_WEBVIEW_POOLS_MESSAGE:\n                        processConfig();\n                        break;\n                    case UrlNavigation.CLEAR_POOLS_MESSAGE:\n                        WebViewPool.this.flushAll();\n                        break;\n                }\n            }\n        };\n        LocalBroadcastManager.getInstance(this.context).registerReceiver(\n                messageReceiver, new IntentFilter(UrlNavigation.STARTED_LOADING_MESSAGE));\n        LocalBroadcastManager.getInstance(this.context).registerReceiver(\n                messageReceiver, new IntentFilter(UrlNavigation.FINISHED_LOADING_MESSAGE));\n        LocalBroadcastManager.getInstance(this.context).registerReceiver(\n                messageReceiver, new IntentFilter(UrlNavigation.CLEAR_POOLS_MESSAGE));\n        LocalBroadcastManager.getInstance(this.context).registerReceiver(\n                messageReceiver, new IntentFilter(AppConfig.PROCESSED_WEBVIEW_POOLS_MESSAGE));\n\n        processConfig();\n    }\n\n    private void processConfig() {\n        JSONArray config = AppConfig.getInstance(this.context).webviewPools;\n        if (config == null) {\n            return;\n        }\n\n        for (int i = 0; i < config.length(); i++) {\n            JSONObject entry = config.optJSONObject(i);\n            if (entry != null) {\n                JSONArray urls = entry.optJSONArray(\"urls\");\n                if (urls != null) {\n                    HashSet<String> urlSet = new HashSet<>();\n                    for (int j = 0; j < urls.length(); j++) {\n                        if (urls.isNull(j)) continue;\n                        String urlString = null;\n                        WebViewPoolDisownPolicy policy = WebViewPoolDisownPolicy.defaultPolicy;\n\n                        Object urlEntry = urls.opt(j);\n                        if (urlEntry instanceof String) urlString = (String)urlEntry;\n\n                        if (urlString == null && urlEntry instanceof JSONObject) {\n                            urlString = ((JSONObject)urlEntry).optString(\"url\");\n                            String policyString = AppConfig.optString((JSONObject)urlEntry, \"disown\");\n                            if (policyString != null) {\n                                if (policyString.equalsIgnoreCase(\"reload\"))\n                                    policy = WebViewPoolDisownPolicy.Reload;\n                                else if (policyString.equalsIgnoreCase(\"never\"))\n                                    policy = WebViewPoolDisownPolicy.Never;\n                                else if (policyString.equalsIgnoreCase(\"always\"))\n                                    policy = WebViewPoolDisownPolicy.Always;\n                            }\n                        }\n\n                        if (urlString != null) {\n                            urlSet.add(urlString);\n                            this.urlToDisownPolicy.put(urlString, policy);\n                        }\n                    }\n\n                    this.urlSets.add(urlSet);\n                }\n\n            }\n        }\n\n        // if config changed, we may have to load webviews corresponding to the previously requested url\n        if (this.lastUrlRequest != null) {\n            webviewForUrl(this.lastUrlRequest);\n        }\n\n        resumeLoading();\n    }\n\n    private void resumeLoading() {\n        if (this.isMainActivityLoading || this.isLoading) return;\n\n        if (this.currentLoadingWebview != null && this.currentLoadingUrl != null) {\n            context.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    currentLoadingWebview.loadUrl(currentLoadingUrl);\n                }\n            });\n            this.isLoading = true;\n            return;\n        }\n\n        if (!this.urlsToLoad.isEmpty()) {\n            final String urlString = this.urlsToLoad.iterator().next();\n            this.currentLoadingUrl = urlString;\n            this.htmlIntercept.setInterceptUrl(urlString);\n\n            context.runOnUiThread(new Runnable() {\n                @Override\n                public void run() {\n                    LeanWebView webview = new LeanWebView(context);\n                    currentLoadingWebview = webview;\n                    urlsToLoad.remove(urlString);\n                    WebViewSetup.setupWebview(webview, context);\n\n                    // size it before loading url\n                    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);\n                    if (wm != null) {\n                        Display display = wm.getDefaultDisplay();\n                        Point size = new Point();\n                        display.getSize(size);\n                        webview.layout(0, 0, size.x, size.y);\n                    }\n\n                    new PoolWebViewClient(webViewPoolCallback, webview);\n\n                    currentLoadingWebview = webview;\n                    urlsToLoad.remove(urlString);\n\n                    currentLoadingWebview.loadUrl(urlString);\n                }\n            });\n        }\n    }\n\n    private void flushAll() {\n        if (this.currentLoadingWebview != null) this.currentLoadingWebview.stopLoading();\n        this.isLoading = false;\n        this.currentLoadingWebview = null;\n        this.currentLoadingUrl = null;\n        this.lastUrlRequest = null;\n        this.urlToWebview.clear();\n    }\n\n    public void disownWebview(GoNativeWebviewInterface webview) {\n        Iterator<String> it = this.urlToWebview.keySet().iterator();\n        while(it.hasNext()) {\n            String key = it.next();\n            if (this.urlToWebview.get(key) == webview) {\n                it.remove();\n                this.urlsToLoad.add(key);\n            }\n        }\n    }\n\n    public Pair<GoNativeWebviewInterface, WebViewPoolDisownPolicy> webviewForUrl(String url) {\n        this.lastUrlRequest = url;\n        HashSet<String> urlSet = urlSetForUrl(url);\n        if (urlSet.size() > 0) {\n            HashSet<String> newUrls = new HashSet<> (urlSet);\n            if (this.currentLoadingUrl != null) {\n                newUrls.remove(this.currentLoadingUrl);\n            }\n            newUrls.removeAll(this.urlToWebview.keySet());\n\n            this.urlsToLoad.addAll(newUrls);\n        }\n\n        GoNativeWebviewInterface webview = this.urlToWebview.get(url);\n        if (webview == null) return new Pair<>(null, null);\n\n        WebViewPoolDisownPolicy policy = this.urlToDisownPolicy.get(url);\n        return new Pair<>(webview, policy);\n    }\n\n    private HashSet<String> urlSetForUrl(String url){\n        HashSet<String> result = new HashSet<>();\n        for (Set<String> set : this.urlSets) {\n            if (set.contains(url)) {\n                result.addAll(set);\n            }\n        }\n        return result;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/WebViewPoolDisownPolicy.java",
    "content": "package io.gonative.android;\n\n/**\n * Created by weiyin on 9/3/14.\n */\npublic enum WebViewPoolDisownPolicy {\n    Always, Reload, Never;\n\n    public static WebViewPoolDisownPolicy defaultPolicy = WebViewPoolDisownPolicy.Reload;\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/files/CapturedImageSaver.kt",
    "content": "package io.gonative.android.files\n\nimport android.content.ContentResolver\nimport android.content.ContentValues\nimport android.content.Context\nimport android.net.Uri\nimport android.os.Build\nimport android.os.Environment\nimport android.provider.MediaStore\nimport androidx.core.content.FileProvider\nimport java.io.File\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\n\n\nclass CapturedImageSaver {\n    fun saveCapturedBitmap(context: Context, bitmapUri: Uri): Uri? {\n        val timeStamp = SimpleDateFormat(\"yyyyMMdd_HHmmss\", Locale.US).format(Date())\n        val imageFileName = \"IMG_$timeStamp.jpg\"\n\n        val resolver: ContentResolver = context.contentResolver\n        val currentUri = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {\n            val contentValues = ContentValues()\n            contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, imageFileName)\n            contentValues.put(MediaStore.MediaColumns.MIME_TYPE, \"image/*\")\n            contentValues.put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)\n            resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)\n        } else {\n            val storageDir = Environment.getExternalStoragePublicDirectory(\n                    Environment.DIRECTORY_PICTURES)\n            val captureFile = File(storageDir, imageFileName)\n            FileProvider.getUriForFile(context, context.applicationContext.packageName + \".fileprovider\", captureFile);\n        }\n\n        currentUri?.let {\n            context.contentResolver.openOutputStream(it).use { output ->\n                context.contentResolver.openInputStream(bitmapUri).use { input ->\n                    output?.write(input?.readBytes())\n                }\n            }\n        }\n\n        return currentUri\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/CircleImageView.java",
    "content": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.graphics.Canvas;\nimport android.graphics.Color;\nimport android.graphics.Paint;\nimport android.graphics.RadialGradient;\nimport android.graphics.Shader;\nimport android.graphics.drawable.ShapeDrawable;\nimport android.graphics.drawable.shapes.OvalShape;\nimport android.view.View;\nimport android.view.animation.Animation;\n\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.ViewCompat;\n\n/**\n * Private class created to work around issues with AnimationListeners being\n * called before the animation is actually complete and support shadows on older\n * platforms.\n */\nclass CircleImageView extends androidx.appcompat.widget.AppCompatImageView {\n    \n    private static final int KEY_SHADOW_COLOR = 0x1E000000;\n    private static final int FILL_SHADOW_COLOR = 0x3D000000;\n    // PX\n    private static final float X_OFFSET = 0f;\n    private static final float Y_OFFSET = 1.75f;\n    private static final float SHADOW_RADIUS = 3.5f;\n    private static final int SHADOW_ELEVATION = 4;\n    \n    private Animation.AnimationListener mListener;\n    int mShadowRadius;\n    \n    CircleImageView(Context context, int color) {\n        super(context);\n        final float density = getContext().getResources().getDisplayMetrics().density;\n        final int shadowYOffset = (int) (density * Y_OFFSET);\n        final int shadowXOffset = (int) (density * X_OFFSET);\n        \n        mShadowRadius = (int) (density * SHADOW_RADIUS);\n        \n        ShapeDrawable circle;\n        if (elevationSupported()) {\n            circle = new ShapeDrawable(new OvalShape());\n            ViewCompat.setElevation(this, SHADOW_ELEVATION * density);\n        } else {\n            OvalShape oval = new CircleImageView.OvalShadow(mShadowRadius);\n            circle = new ShapeDrawable(oval);\n            setLayerType(View.LAYER_TYPE_SOFTWARE, circle.getPaint());\n            circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset,\n                    KEY_SHADOW_COLOR);\n            final int padding = mShadowRadius;\n            // set padding so the inner image sits correctly within the shadow.\n            setPadding(padding, padding, padding, padding);\n        }\n        circle.getPaint().setColor(color);\n        ViewCompat.setBackground(this, circle);\n    }\n    \n    private boolean elevationSupported() {\n        return android.os.Build.VERSION.SDK_INT >= 21;\n    }\n    \n    @Override\n    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        if (!elevationSupported()) {\n            setMeasuredDimension(getMeasuredWidth() + mShadowRadius * 2, getMeasuredHeight()\n                    + mShadowRadius * 2);\n        }\n    }\n    \n    public void setAnimationListener(Animation.AnimationListener listener) {\n        mListener = listener;\n    }\n    \n    @Override\n    public void onAnimationStart() {\n        super.onAnimationStart();\n        if (mListener != null) {\n            mListener.onAnimationStart(getAnimation());\n        }\n    }\n    \n    @Override\n    public void onAnimationEnd() {\n        super.onAnimationEnd();\n        if (mListener != null) {\n            mListener.onAnimationEnd(getAnimation());\n        }\n    }\n    \n    /**\n     * Update the background color of the circle image view.\n     *\n     * @param colorRes Id of a color resource.\n     */\n    public void setBackgroundColorRes(int colorRes) {\n        setBackgroundColor(ContextCompat.getColor(getContext(), colorRes));\n    }\n    \n    @Override\n    public void setBackgroundColor(int color) {\n        if (getBackground() instanceof ShapeDrawable) {\n            ((ShapeDrawable) getBackground()).getPaint().setColor(color);\n        }\n    }\n    \n    private class OvalShadow extends OvalShape {\n        private RadialGradient mRadialGradient;\n        private Paint mShadowPaint;\n        \n        OvalShadow(int shadowRadius) {\n            super();\n            mShadowPaint = new Paint();\n            mShadowRadius = shadowRadius;\n            updateRadialGradient((int) rect().width());\n        }\n        \n        @Override\n        protected void onResize(float width, float height) {\n            super.onResize(width, height);\n            updateRadialGradient((int) width);\n        }\n        \n        @Override\n        public void draw(Canvas canvas, Paint paint) {\n            final int viewWidth = CircleImageView.this.getWidth();\n            final int viewHeight = CircleImageView.this.getHeight();\n            canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2, mShadowPaint);\n            canvas.drawCircle(viewWidth / 2, viewHeight / 2, viewWidth / 2 - mShadowRadius, paint);\n        }\n        \n        private void updateRadialGradient(int diameter) {\n            mRadialGradient = new RadialGradient(diameter / 2, diameter / 2,\n                    mShadowRadius, new int[] { FILL_SHADOW_COLOR, Color.TRANSPARENT },\n                    null, Shader.TileMode.CLAMP);\n            mShadowPaint.setShader(mRadialGradient);\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/GoNativeDrawerLayout.java",
    "content": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.util.Log;\nimport android.view.MotionEvent;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.drawerlayout.widget.DrawerLayout;\n\npublic class GoNativeDrawerLayout extends DrawerLayout {\n    \n    private boolean disableTouch = false;\n    \n    public GoNativeDrawerLayout(@NonNull Context context) {\n        this(context, null);\n    }\n    \n    public GoNativeDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs) {\n        this(context, attrs, 0);\n    }\n    \n    public GoNativeDrawerLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n    }\n    \n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        if (disableTouch) {\n            Log.d(\"SWIPE\", \"GNDrawerLayout disabled touch\");\n            return false;\n        }\n        return super.onInterceptTouchEvent(ev);\n    }\n    \n    \n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        if (disableTouch) {\n            Log.d(\"SWIPE\", \"GNDrawerLayout disabled touch\");\n            return false;\n        }\n        return super.onTouchEvent(ev);\n    }\n    \n    public void setDisableTouch(boolean disableTouch){\n        this.disableTouch = disableTouch;\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/GoNativeSwipeRefreshLayout.java",
    "content": "package io.gonative.android.widget;\n\nimport android.content.Context;\nimport android.content.res.TypedArray;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.view.MotionEvent;\nimport android.view.View;\nimport android.view.ViewConfiguration;\nimport android.view.ViewGroup;\nimport android.view.animation.Animation;\nimport android.view.animation.DecelerateInterpolator;\nimport android.view.animation.Transformation;\nimport android.widget.AbsListView;\nimport android.widget.ListView;\n\nimport androidx.annotation.ColorInt;\nimport androidx.annotation.ColorRes;\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\nimport androidx.annotation.Px;\nimport androidx.annotation.VisibleForTesting;\nimport androidx.core.content.ContextCompat;\nimport androidx.core.view.NestedScrollingChild;\nimport androidx.core.view.NestedScrollingChildHelper;\nimport androidx.core.view.NestedScrollingParent;\nimport androidx.core.view.NestedScrollingParentHelper;\nimport androidx.core.view.ViewCompat;\nimport androidx.core.widget.ListViewCompat;\nimport androidx.swiperefreshlayout.widget.CircularProgressDrawable;\n\nimport io.gonative.gonative_core.GNLog;\n\n/**\n * The SwipeRefreshLayout should be used whenever the user can refresh the\n * contents of a view via a vertical swipe gesture. The activity that\n * instantiates this view should add an OnRefreshListener to be notified\n * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout\n * will notify the listener each and every time the gesture is completed again;\n * the listener is responsible for correctly determining when to actually\n * initiate a refresh of its content. If the listener determines there should\n * not be a refresh, it must call setRefreshing(false) to cancel any visual\n * indication of a refresh. If an activity wishes to show just the progress\n * animation, it should call setRefreshing(true). To disable the gesture and\n * progress animation, call setEnabled(false) on the view.\n * <p>\n * This layout should be made the parent of the view that will be refreshed as a\n * result of the gesture and can only support one direct child. This view will\n * also be made the target of the gesture and will be forced to match both the\n * width and the height supplied in this layout. The SwipeRefreshLayout does not\n * provide accessibility events; instead, a menu item must be provided to allow\n * refresh of the content wherever this gesture is used.\n * </p>\n */\npublic class GoNativeSwipeRefreshLayout extends ViewGroup implements NestedScrollingParent,\n        NestedScrollingChild {\n    // Maps to ProgressBar.Large style\n    public static final int LARGE = CircularProgressDrawable.LARGE;\n    // Maps to ProgressBar default style\n    public static final int DEFAULT = CircularProgressDrawable.DEFAULT;\n    \n    public static final int DEFAULT_SLINGSHOT_DISTANCE = -1;\n    \n    @VisibleForTesting\n    static final int CIRCLE_DIAMETER = 40;\n    @VisibleForTesting\n    static final int CIRCLE_DIAMETER_LARGE = 56;\n    \n    private static final String LOG_TAG = GoNativeSwipeRefreshLayout.class.getSimpleName();\n    \n    private static final int MAX_ALPHA = 255;\n    private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA);\n    \n    private static final float DECELERATE_INTERPOLATION_FACTOR = 2f;\n    private static final int INVALID_POINTER = -1;\n    private static final float DRAG_RATE = .5f;\n    \n    // Max amount of circle that can be filled by progress during swipe gesture,\n    // where 1.0 is a full circle\n    private static final float MAX_PROGRESS_ANGLE = .8f;\n    \n    private static final int SCALE_DOWN_DURATION = 150;\n    \n    private static final int ALPHA_ANIMATION_DURATION = 300;\n    \n    private static final int ANIMATE_TO_TRIGGER_DURATION = 200;\n    \n    private static final int ANIMATE_TO_START_DURATION = 200;\n    \n    // Default background for the progress spinner\n    private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA;\n    // Default offset in dips from the top of the view to where the progress spinner should stop\n    private static final int DEFAULT_CIRCLE_TARGET = 64;\n    \n    private View mTarget; // the target of the gesture\n    GoNativeSwipeRefreshLayout.OnRefreshListener mListener;\n    boolean mRefreshing = false;\n    private int mTouchSlop;\n    private float mTotalDragDistance = -1;\n    \n    // If nested scrolling is enabled, the total amount that needed to be\n    // consumed by this as the nested scrolling parent is used in place of the\n    // overscroll determined by MOVE events in the onTouch handler\n    private float mTotalUnconsumed;\n    private final NestedScrollingParentHelper mNestedScrollingParentHelper;\n    private final NestedScrollingChildHelper mNestedScrollingChildHelper;\n    private final int[] mParentScrollConsumed = new int[2];\n    private final int[] mParentOffsetInWindow = new int[2];\n    private boolean mNestedScrollInProgress;\n    \n    private int mMediumAnimationDuration;\n    int mCurrentTargetOffsetTop;\n    \n    private float mInitialMotionY;\n    private float mInitialDownY;\n    private float mInitialDownX;\n    private boolean mIsBeingDragged;\n    private int mActivePointerId = INVALID_POINTER;\n    // Whether this item is scaled up rather than clipped\n    boolean mScale;\n    \n    // Target is returning to its start offset because it was cancelled or a\n    // refresh was triggered.\n    private boolean mReturningToStart;\n    private final DecelerateInterpolator mDecelerateInterpolator;\n    private static final int[] LAYOUT_ATTRS = new int[] {\n            android.R.attr.enabled\n    };\n    \n    CircleImageView mCircleView;\n    private int mCircleViewIndex = -1;\n    \n    protected int mFrom;\n    \n    float mStartingScale;\n    \n    protected int mOriginalOffsetTop;\n    \n    int mSpinnerOffsetEnd;\n    \n    int mCustomSlingshotDistance;\n    \n    CircularProgressDrawable mProgress;\n    \n    private Animation mScaleAnimation;\n    \n    private Animation mScaleDownAnimation;\n    \n    private Animation mAlphaStartAnimation;\n    \n    private Animation mAlphaMaxAnimation;\n    \n    private Animation mScaleDownToStartAnimation;\n    \n    boolean mNotify;\n    \n    private int mCircleDiameter;\n    \n    // Whether the client has set a custom starting position;\n    boolean mUsingCustomStart;\n    \n    private GoNativeSwipeRefreshLayout.OnChildScrollUpCallback mChildScrollUpCallback;\n    \n    private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() {\n        @Override\n        public void onAnimationStart(Animation animation) {\n        }\n        \n        @Override\n        public void onAnimationRepeat(Animation animation) {\n        }\n        \n        @Override\n        public void onAnimationEnd(Animation animation) {\n            if (mRefreshing) {\n                // Make sure the progress view is fully visible\n                mProgress.setAlpha(MAX_ALPHA);\n                mProgress.start();\n                if (mNotify) {\n                    if (mListener != null) {\n                        mListener.onRefresh();\n                    }\n                }\n                mCurrentTargetOffsetTop = mCircleView.getTop();\n            } else {\n                reset();\n            }\n        }\n    };\n    \n    void reset() {\n        mCircleView.clearAnimation();\n        mProgress.stop();\n        mCircleView.setVisibility(View.GONE);\n        setColorViewAlpha(MAX_ALPHA);\n        // Return the circle to its start position\n        if (mScale) {\n            setAnimationProgress(0 /* animation complete and view is hidden */);\n        } else {\n            setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop);\n        }\n        mCurrentTargetOffsetTop = mCircleView.getTop();\n    }\n    \n    @Override\n    public void setEnabled(boolean enabled) {\n        super.setEnabled(enabled);\n        if (!enabled) {\n            reset();\n        }\n    }\n    \n    @Override\n    protected void onDetachedFromWindow() {\n        super.onDetachedFromWindow();\n        reset();\n    }\n    \n    private void setColorViewAlpha(int targetAlpha) {\n        mCircleView.getBackground().setAlpha(targetAlpha);\n        mProgress.setAlpha(targetAlpha);\n    }\n    \n    /**\n     * The refresh indicator starting and resting position is always positioned\n     * near the top of the refreshing content. This position is a consistent\n     * location, but can be adjusted in either direction based on whether or not\n     * there is a toolbar or actionbar present.\n     * <p>\n     * <strong>Note:</strong> Calling this will reset the position of the refresh indicator to\n     * <code>start</code>.\n     * </p>\n     *\n     * @param scale Set to true if there is no view at a higher z-order than where the progress\n     *              spinner is set to appear. Setting it to true will cause indicator to be scaled\n     *              up rather than clipped.\n     * @param start The offset in pixels from the top of this view at which the\n     *              progress spinner should appear.\n     * @param end The offset in pixels from the top of this view at which the\n     *            progress spinner should come to rest after a successful swipe\n     *            gesture.\n     */\n    public void setProgressViewOffset(boolean scale, int start, int end) {\n        mScale = scale;\n        mOriginalOffsetTop = start;\n        mSpinnerOffsetEnd = end;\n        mUsingCustomStart = true;\n        reset();\n        mRefreshing = false;\n    }\n    \n    /**\n     * @return The offset in pixels from the top of this view at which the progress spinner should\n     *         appear.\n     */\n    public int getProgressViewStartOffset() {\n        return mOriginalOffsetTop;\n    }\n    \n    /**\n     * @return The offset in pixels from the top of this view at which the progress spinner should\n     *         come to rest after a successful swipe gesture.\n     */\n    public int getProgressViewEndOffset() {\n        return mSpinnerOffsetEnd;\n    }\n    \n    /**\n     * The refresh indicator resting position is always positioned near the top\n     * of the refreshing content. This position is a consistent location, but\n     * can be adjusted in either direction based on whether or not there is a\n     * toolbar or actionbar present.\n     *\n     * @param scale Set to true if there is no view at a higher z-order than where the progress\n     *              spinner is set to appear. Setting it to true will cause indicator to be scaled\n     *              up rather than clipped.\n     * @param end The offset in pixels from the top of this view at which the\n     *            progress spinner should come to rest after a successful swipe\n     *            gesture.\n     */\n    public void setProgressViewEndTarget(boolean scale, int end) {\n        mSpinnerOffsetEnd = end;\n        mScale = scale;\n        mCircleView.invalidate();\n    }\n    \n    /**\n     * Sets a custom slingshot distance.\n     *\n     * @param slingshotDistance The distance in pixels that the refresh indicator can be pulled\n     *                          beyond its resting position. Use\n     *                          {@link #DEFAULT_SLINGSHOT_DISTANCE} to reset to the default value.\n     *\n     */\n    public void setSlingshotDistance(@Px int slingshotDistance) {\n        mCustomSlingshotDistance = slingshotDistance;\n    }\n    \n    /**\n     * One of DEFAULT, or LARGE.\n     */\n    public void setSize(int size) {\n        if (size != CircularProgressDrawable.LARGE && size != CircularProgressDrawable.DEFAULT) {\n            return;\n        }\n        final DisplayMetrics metrics = getResources().getDisplayMetrics();\n        if (size == CircularProgressDrawable.LARGE) {\n            mCircleDiameter = (int) (CIRCLE_DIAMETER_LARGE * metrics.density);\n        } else {\n            mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);\n        }\n        // force the bounds of the progress circle inside the circle view to\n        // update by setting it to null before updating its size and then\n        // re-setting it\n        mCircleView.setImageDrawable(null);\n        mProgress.setStyle(size);\n        mCircleView.setImageDrawable(mProgress);\n    }\n    \n    /**\n     * Simple constructor to use when creating a SwipeRefreshLayout from code.\n     *\n     * @param context\n     */\n    public GoNativeSwipeRefreshLayout(@NonNull Context context) {\n        this(context, null);\n    }\n    \n    /**\n     * Constructor that is called when inflating SwipeRefreshLayout from XML.\n     *\n     * @param context\n     * @param attrs\n     */\n    public GoNativeSwipeRefreshLayout(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        \n        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();\n        \n        mMediumAnimationDuration = getResources().getInteger(\n                android.R.integer.config_mediumAnimTime);\n        \n        setWillNotDraw(false);\n        mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR);\n        \n        final DisplayMetrics metrics = getResources().getDisplayMetrics();\n        mCircleDiameter = (int) (CIRCLE_DIAMETER * metrics.density);\n        \n        createProgressView();\n        setChildrenDrawingOrderEnabled(true);\n        // the absolute offset has to take into account that the circle starts at an offset\n        mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density);\n        mTotalDragDistance = mSpinnerOffsetEnd;\n        mNestedScrollingParentHelper = new NestedScrollingParentHelper(this);\n        \n        mNestedScrollingChildHelper = new NestedScrollingChildHelper(this);\n        setNestedScrollingEnabled(true);\n        \n        mOriginalOffsetTop = mCurrentTargetOffsetTop = -mCircleDiameter;\n        moveToStart(1.0f);\n        \n        final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS);\n        setEnabled(a.getBoolean(0, true));\n        a.recycle();\n    }\n    \n    @Override\n    protected int getChildDrawingOrder(int childCount, int i) {\n        if (mCircleViewIndex < 0) {\n            return i;\n        } else if (i == childCount - 1) {\n            // Draw the selected child last\n            return mCircleViewIndex;\n        } else if (i >= mCircleViewIndex) {\n            // Move the children after the selected child earlier one\n            return i + 1;\n        } else {\n            // Keep the children before the selected child the same\n            return i;\n        }\n    }\n    \n    private void createProgressView() {\n        mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT);\n        mProgress = new CircularProgressDrawable(getContext());\n        mProgress.setStyle(CircularProgressDrawable.DEFAULT);\n        mCircleView.setImageDrawable(mProgress);\n        mCircleView.setVisibility(View.GONE);\n        addView(mCircleView);\n    }\n    \n    /**\n     * Set the listener to be notified when a refresh is triggered via the swipe\n     * gesture.\n     */\n    public void setOnRefreshListener(@Nullable GoNativeSwipeRefreshLayout.OnRefreshListener listener) {\n        mListener = listener;\n    }\n    \n    /**\n     * Notify the widget that refresh state has changed. Do not call this when\n     * refresh is triggered by a swipe gesture.\n     *\n     * @param refreshing Whether or not the view should show refresh progress.\n     */\n    public void setRefreshing(boolean refreshing) {\n        if (refreshing && mRefreshing != refreshing) {\n            // scale and show\n            mRefreshing = refreshing;\n            int endTarget = 0;\n            if (!mUsingCustomStart) {\n                endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop;\n            } else {\n                endTarget = mSpinnerOffsetEnd;\n            }\n            setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop);\n            mNotify = false;\n            startScaleUpAnimation(mRefreshListener);\n        } else {\n            setRefreshing(refreshing, false /* notify */);\n        }\n    }\n    \n    private void startScaleUpAnimation(Animation.AnimationListener listener) {\n        mCircleView.setVisibility(View.VISIBLE);\n        mProgress.setAlpha(MAX_ALPHA);\n        mScaleAnimation = new Animation() {\n            @Override\n            public void applyTransformation(float interpolatedTime, Transformation t) {\n                setAnimationProgress(interpolatedTime);\n            }\n        };\n        mScaleAnimation.setDuration(mMediumAnimationDuration);\n        if (listener != null) {\n            mCircleView.setAnimationListener(listener);\n        }\n        mCircleView.clearAnimation();\n        mCircleView.startAnimation(mScaleAnimation);\n    }\n    \n    /**\n     * Pre API 11, this does an alpha animation.\n     * @param progress\n     */\n    void setAnimationProgress(float progress) {\n        mCircleView.setScaleX(progress);\n        mCircleView.setScaleY(progress);\n    }\n    \n    private void setRefreshing(boolean refreshing, final boolean notify) {\n        if (mRefreshing != refreshing) {\n            mNotify = notify;\n            ensureTarget();\n            mRefreshing = refreshing;\n            if (mRefreshing) {\n                animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener);\n            } else {\n                startScaleDownAnimation(mRefreshListener);\n            }\n        }\n    }\n    \n    void startScaleDownAnimation(Animation.AnimationListener listener) {\n        mScaleDownAnimation = new Animation() {\n            @Override\n            public void applyTransformation(float interpolatedTime, Transformation t) {\n                setAnimationProgress(1 - interpolatedTime);\n            }\n        };\n        mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION);\n        mCircleView.setAnimationListener(listener);\n        mCircleView.clearAnimation();\n        mCircleView.startAnimation(mScaleDownAnimation);\n    }\n    \n    private void startProgressAlphaStartAnimation() {\n        mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA);\n    }\n    \n    private void startProgressAlphaMaxAnimation() {\n        mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA);\n    }\n    \n    private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) {\n        Animation alpha = new Animation() {\n            @Override\n            public void applyTransformation(float interpolatedTime, Transformation t) {\n                mProgress.setAlpha(\n                        (int) (startingAlpha + ((endingAlpha - startingAlpha) * interpolatedTime)));\n            }\n        };\n        alpha.setDuration(ALPHA_ANIMATION_DURATION);\n        // Clear out the previous animation listeners.\n        mCircleView.setAnimationListener(null);\n        mCircleView.clearAnimation();\n        mCircleView.startAnimation(alpha);\n        return alpha;\n    }\n    \n    /**\n     * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)}\n     */\n    @Deprecated\n    public void setProgressBackgroundColor(int colorRes) {\n        setProgressBackgroundColorSchemeResource(colorRes);\n    }\n    \n    /**\n     * Set the background color of the progress spinner disc.\n     *\n     * @param colorRes Resource id of the color.\n     */\n    public void setProgressBackgroundColorSchemeResource(@ColorRes int colorRes) {\n        setProgressBackgroundColorSchemeColor(ContextCompat.getColor(getContext(), colorRes));\n    }\n    \n    /**\n     * Set the background color of the progress spinner disc.\n     *\n     * @param color\n     */\n    public void setProgressBackgroundColorSchemeColor(@ColorInt int color) {\n        mCircleView.setBackgroundColor(color);\n    }\n    \n    /**\n     * @deprecated Use {@link #setColorSchemeResources(int...)}\n     */\n    @Deprecated\n    public void setColorScheme(@ColorRes int... colors) {\n        setColorSchemeResources(colors);\n    }\n    \n    /**\n     * Set the color resources used in the progress animation from color resources.\n     * The first color will also be the color of the bar that grows in response\n     * to a user swipe gesture.\n     *\n     * @param colorResIds\n     */\n    public void setColorSchemeResources(@ColorRes int... colorResIds) {\n        final Context context = getContext();\n        int[] colorRes = new int[colorResIds.length];\n        for (int i = 0; i < colorResIds.length; i++) {\n            colorRes[i] = ContextCompat.getColor(context, colorResIds[i]);\n        }\n        setColorSchemeColors(colorRes);\n    }\n    \n    /**\n     * Set the colors used in the progress animation. The first\n     * color will also be the color of the bar that grows in response to a user\n     * swipe gesture.\n     *\n     * @param colors\n     */\n    public void setColorSchemeColors(@ColorInt int... colors) {\n        ensureTarget();\n        mProgress.setColorSchemeColors(colors);\n    }\n    \n    /**\n     * @return Whether the SwipeRefreshWidget is actively showing refresh\n     *         progress.\n     */\n    public boolean isRefreshing() {\n        return mRefreshing;\n    }\n    \n    private void ensureTarget() {\n        // Don't bother getting the parent height if the parent hasn't been laid\n        // out yet.\n        if (mTarget == null) {\n            for (int i = 0; i < getChildCount(); i++) {\n                View child = getChildAt(i);\n                if (!child.equals(mCircleView)) {\n                    mTarget = child;\n                    break;\n                }\n            }\n        }\n    }\n    \n    /**\n     * Set the distance to trigger a sync in dips\n     *\n     * @param distance\n     */\n    public void setDistanceToTriggerSync(int distance) {\n        mTotalDragDistance = distance;\n    }\n    \n    @Override\n    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {\n        final int width = getMeasuredWidth();\n        final int height = getMeasuredHeight();\n        if (getChildCount() == 0) {\n            return;\n        }\n        if (mTarget == null) {\n            ensureTarget();\n        }\n        if (mTarget == null) {\n            return;\n        }\n        final View child = mTarget;\n        final int childLeft = getPaddingLeft();\n        final int childTop = getPaddingTop();\n        final int childWidth = width - getPaddingLeft() - getPaddingRight();\n        final int childHeight = height - getPaddingTop() - getPaddingBottom();\n        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);\n        int circleWidth = mCircleView.getMeasuredWidth();\n        int circleHeight = mCircleView.getMeasuredHeight();\n        mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop,\n                (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight);\n    }\n    \n    @Override\n    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {\n        super.onMeasure(widthMeasureSpec, heightMeasureSpec);\n        if (mTarget == null) {\n            ensureTarget();\n        }\n        if (mTarget == null) {\n            return;\n        }\n        mTarget.measure(MeasureSpec.makeMeasureSpec(\n                getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),\n                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(\n                getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY));\n        mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY),\n                MeasureSpec.makeMeasureSpec(mCircleDiameter, MeasureSpec.EXACTLY));\n        mCircleViewIndex = -1;\n        // Get the index of the circleview.\n        for (int index = 0; index < getChildCount(); index++) {\n            if (getChildAt(index) == mCircleView) {\n                mCircleViewIndex = index;\n                break;\n            }\n        }\n    }\n    \n    /**\n     * Get the diameter of the progress circle that is displayed as part of the\n     * swipe to refresh layout.\n     *\n     * @return Diameter in pixels of the progress circle view.\n     */\n    public int getProgressCircleDiameter() {\n        return mCircleDiameter;\n    }\n    \n    /**\n     * @return Whether it is possible for the child view of this layout to\n     *         scroll up. Override this if the child view is a custom view.\n     */\n    public boolean canChildScrollUp() {\n        if (mChildScrollUpCallback != null) {\n            return mChildScrollUpCallback.canChildScrollUp(this, mTarget);\n        }\n        if (mTarget instanceof ListView) {\n            return ListViewCompat.canScrollList((ListView) mTarget, -1);\n        }\n        return mTarget.canScrollVertically(-1);\n    }\n    \n    /**\n     * Set a callback to override {@link GoNativeSwipeRefreshLayout#canChildScrollUp()} method. Non-null\n     * callback will return the value provided by the callback and ignore all internal logic.\n     * @param callback Callback that should be called when canChildScrollUp() is called.\n     */\n    public void setOnChildScrollUpCallback(@Nullable GoNativeSwipeRefreshLayout.OnChildScrollUpCallback callback) {\n        mChildScrollUpCallback = callback;\n    }\n    \n    @Override\n    public boolean onInterceptTouchEvent(MotionEvent ev) {\n        ensureTarget();\n        \n        final int action = ev.getActionMasked();\n        int pointerIndex;\n        \n        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {\n            mReturningToStart = false;\n        }\n        \n        if (!isEnabled() || mReturningToStart || canChildScrollUp()\n                || mRefreshing || mNestedScrollInProgress || ev.getPointerCount() > 1) {\n            // Fail fast if we're not in a state where a swipe is possible\n            return false;\n        }\n        \n        switch (action) {\n            case MotionEvent.ACTION_DOWN:\n                setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop());\n                mActivePointerId = ev.getPointerId(0);\n                mIsBeingDragged = false;\n                \n                pointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (pointerIndex < 0) {\n                    return false;\n                }\n                mInitialDownY = ev.getY(pointerIndex);\n                mInitialDownX = ev.getX(pointerIndex);\n                break;\n            \n            case MotionEvent.ACTION_MOVE:\n                if (mActivePointerId == INVALID_POINTER) {\n                    GNLog.getInstance().logError(LOG_TAG, \"Got ACTION_MOVE event but don't have an active pointer id.\");\n                    return false;\n                }\n                \n                pointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (pointerIndex < 0) {\n                    return false;\n                }\n    \n                // START: modification\n                \n                float diffX = ev.getX() - mInitialDownX;\n                float diffY = ev.getY() - mInitialDownY;\n                if (Math.abs(diffY) < Math.abs(diffX)) {\n                    // horizontal scroll\n                    break;\n                }\n                \n                // END: modification\n                \n                final float y = ev.getY(pointerIndex);\n                startDragging(y);\n                break;\n            \n            case MotionEvent.ACTION_POINTER_UP:\n                onSecondaryPointerUp(ev);\n                break;\n            \n            case MotionEvent.ACTION_UP:\n            case MotionEvent.ACTION_CANCEL:\n                mIsBeingDragged = false;\n                mActivePointerId = INVALID_POINTER;\n                break;\n        }\n        \n        return mIsBeingDragged;\n    }\n    \n    @Override\n    public void requestDisallowInterceptTouchEvent(boolean b) {\n        // if this is a List < L or another view that doesn't support nested\n        // scrolling, ignore this request so that the vertical scroll event\n        // isn't stolen\n        if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView)\n                || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) {\n            // Nope.\n        } else {\n            super.requestDisallowInterceptTouchEvent(b);\n        }\n    }\n    \n    // NestedScrollingParent\n    \n    @Override\n    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {\n        return isEnabled() && !mReturningToStart && !mRefreshing\n                && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;\n    }\n    \n    @Override\n    public void onNestedScrollAccepted(View child, View target, int axes) {\n        // Reset the counter of how much leftover scroll needs to be consumed.\n        mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes);\n        // Dispatch up to the nested parent\n        startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);\n        mTotalUnconsumed = 0;\n        mNestedScrollInProgress = true;\n    }\n    \n    @Override\n    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {\n        // If we are in the middle of consuming, a scroll, then we want to move the spinner back up\n        // before allowing the list to scroll\n        if (dy > 0 && mTotalUnconsumed > 0) {\n            if (dy > mTotalUnconsumed) {\n                consumed[1] = dy - (int) mTotalUnconsumed;\n                mTotalUnconsumed = 0;\n            } else {\n                mTotalUnconsumed -= dy;\n                consumed[1] = dy;\n            }\n            moveSpinner(mTotalUnconsumed);\n        }\n        \n        // If a client layout is using a custom start position for the circle\n        // view, they mean to hide it again before scrolling the child view\n        // If we get back to mTotalUnconsumed == 0 and there is more to go, hide\n        // the circle so it isn't exposed if its blocking content is moved\n        if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0\n                && Math.abs(dy - consumed[1]) > 0) {\n            mCircleView.setVisibility(View.GONE);\n        }\n        \n        // Now let our nested parent consume the leftovers\n        final int[] parentConsumed = mParentScrollConsumed;\n        if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) {\n            consumed[0] += parentConsumed[0];\n            consumed[1] += parentConsumed[1];\n        }\n    }\n    \n    @Override\n    public int getNestedScrollAxes() {\n        return mNestedScrollingParentHelper.getNestedScrollAxes();\n    }\n    \n    @Override\n    public void onStopNestedScroll(View target) {\n        mNestedScrollingParentHelper.onStopNestedScroll(target);\n        mNestedScrollInProgress = false;\n        // Finish the spinner for nested scrolling if we ever consumed any\n        // unconsumed nested scroll\n        if (mTotalUnconsumed > 0) {\n            finishSpinner(mTotalUnconsumed);\n            mTotalUnconsumed = 0;\n        }\n        // Dispatch up our nested parent\n        stopNestedScroll();\n    }\n    \n    @Override\n    public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed,\n                               final int dxUnconsumed, final int dyUnconsumed) {\n        // Dispatch up to the nested parent first\n        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,\n                mParentOffsetInWindow);\n        \n        // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are\n        // sometimes between two nested scrolling views, we need a way to be able to know when any\n        // nested scrolling parent has stopped handling events. We do that by using the\n        // 'offset in window 'functionality to see if we have been moved from the event.\n        // This is a decent indication of whether we should take over the event stream or not.\n        final int dy = dyUnconsumed + mParentOffsetInWindow[1];\n        if (dy < 0 && !canChildScrollUp()) {\n            mTotalUnconsumed += Math.abs(dy);\n            moveSpinner(mTotalUnconsumed);\n        }\n    }\n    \n    // NestedScrollingChild\n    \n    @Override\n    public void setNestedScrollingEnabled(boolean enabled) {\n        mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled);\n    }\n    \n    @Override\n    public boolean isNestedScrollingEnabled() {\n        return mNestedScrollingChildHelper.isNestedScrollingEnabled();\n    }\n    \n    @Override\n    public boolean startNestedScroll(int axes) {\n        return mNestedScrollingChildHelper.startNestedScroll(axes);\n    }\n    \n    @Override\n    public void stopNestedScroll() {\n        mNestedScrollingChildHelper.stopNestedScroll();\n    }\n    \n    @Override\n    public boolean hasNestedScrollingParent() {\n        return mNestedScrollingChildHelper.hasNestedScrollingParent();\n    }\n    \n    @Override\n    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,\n                                        int dyUnconsumed, int[] offsetInWindow) {\n        return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,\n                dxUnconsumed, dyUnconsumed, offsetInWindow);\n    }\n    \n    @Override\n    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {\n        return mNestedScrollingChildHelper.dispatchNestedPreScroll(\n                dx, dy, consumed, offsetInWindow);\n    }\n    \n    @Override\n    public boolean onNestedPreFling(View target, float velocityX,\n                                    float velocityY) {\n        return dispatchNestedPreFling(velocityX, velocityY);\n    }\n    \n    @Override\n    public boolean onNestedFling(View target, float velocityX, float velocityY,\n                                 boolean consumed) {\n        return dispatchNestedFling(velocityX, velocityY, consumed);\n    }\n    \n    @Override\n    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {\n        return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);\n    }\n    \n    @Override\n    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {\n        return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);\n    }\n    \n    private boolean isAnimationRunning(Animation animation) {\n        return animation != null && animation.hasStarted() && !animation.hasEnded();\n    }\n    \n    private void moveSpinner(float overscrollTop) {\n        mProgress.setArrowEnabled(true);\n        float originalDragPercent = overscrollTop / mTotalDragDistance;\n        \n        float dragPercent = Math.min(1f, Math.abs(originalDragPercent));\n        float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3;\n        float extraOS = Math.abs(overscrollTop) - mTotalDragDistance;\n        float slingshotDist = mCustomSlingshotDistance > 0\n                ? mCustomSlingshotDistance\n                : (mUsingCustomStart\n                ? mSpinnerOffsetEnd - mOriginalOffsetTop\n                : mSpinnerOffsetEnd);\n        float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2)\n                / slingshotDist);\n        float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow(\n                (tensionSlingshotPercent / 4), 2)) * 2f;\n        float extraMove = (slingshotDist) * tensionPercent * 2;\n        \n        int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove);\n        // where 1.0f is a full circle\n        if (mCircleView.getVisibility() != View.VISIBLE) {\n            mCircleView.setVisibility(View.VISIBLE);\n        }\n        if (!mScale) {\n            mCircleView.setScaleX(1f);\n            mCircleView.setScaleY(1f);\n        }\n        \n        if (mScale) {\n            setAnimationProgress(Math.min(1f, overscrollTop / mTotalDragDistance));\n        }\n        if (overscrollTop < mTotalDragDistance) {\n            if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA\n                    && !isAnimationRunning(mAlphaStartAnimation)) {\n                // Animate the alpha\n                startProgressAlphaStartAnimation();\n            }\n        } else {\n            if (mProgress.getAlpha() < MAX_ALPHA && !isAnimationRunning(mAlphaMaxAnimation)) {\n                // Animate the alpha\n                startProgressAlphaMaxAnimation();\n            }\n        }\n        float strokeStart = adjustedPercent * .8f;\n        mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart));\n        mProgress.setArrowScale(Math.min(1f, adjustedPercent));\n        \n        float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f;\n        mProgress.setProgressRotation(rotation);\n        setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop);\n    }\n    \n    private void finishSpinner(float overscrollTop) {\n        if (overscrollTop > mTotalDragDistance) {\n            setRefreshing(true, true /* notify */);\n        } else {\n            // cancel refresh\n            mRefreshing = false;\n            mProgress.setStartEndTrim(0f, 0f);\n            Animation.AnimationListener listener = null;\n            if (!mScale) {\n                listener = new Animation.AnimationListener() {\n                    \n                    @Override\n                    public void onAnimationStart(Animation animation) {\n                    }\n                    \n                    @Override\n                    public void onAnimationEnd(Animation animation) {\n                        if (!mScale) {\n                            startScaleDownAnimation(null);\n                        }\n                    }\n                    \n                    @Override\n                    public void onAnimationRepeat(Animation animation) {\n                    }\n                    \n                };\n            }\n            animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener);\n            mProgress.setArrowEnabled(false);\n        }\n    }\n    \n    @Override\n    public boolean onTouchEvent(MotionEvent ev) {\n        final int action = ev.getActionMasked();\n        int pointerIndex = -1;\n        \n        if (mReturningToStart && action == MotionEvent.ACTION_DOWN) {\n            mReturningToStart = false;\n        }\n        \n        if (!isEnabled() || mReturningToStart || canChildScrollUp()\n                || mRefreshing || mNestedScrollInProgress) {\n            // Fail fast if we're not in a state where a swipe is possible\n            return false;\n        }\n        \n        switch (action) {\n            case MotionEvent.ACTION_DOWN:\n                mActivePointerId = ev.getPointerId(0);\n                mIsBeingDragged = false;\n                break;\n            \n            case MotionEvent.ACTION_MOVE: {\n                pointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (pointerIndex < 0) {\n                    GNLog.getInstance().logError(LOG_TAG, \"Got ACTION_MOVE event but have an invalid active pointer id.\");\n                    return false;\n                }\n                \n                // START: modification\n                \n                float diffX = ev.getX() - mInitialDownX;\n                float diffY = ev.getY() - mInitialDownY;\n                if (Math.abs(diffY) < Math.abs(diffX)) {\n                    // horizontal scroll\n                    break;\n                }\n                \n                // END: modification\n                \n                final float y = ev.getY(pointerIndex);\n                startDragging(y);\n                \n                if (mIsBeingDragged) {\n                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;\n                    if (overscrollTop > 0) {\n                        moveSpinner(overscrollTop);\n                    } else {\n                        return false;\n                    }\n                }\n                break;\n            }\n            case MotionEvent.ACTION_POINTER_DOWN: {\n                pointerIndex = ev.getActionIndex();\n                if (pointerIndex < 0) {\n                    GNLog.getInstance().logError(LOG_TAG,\n                            \"Got ACTION_POINTER_DOWN event but have an invalid action index.\");\n                    return false;\n                }\n                mActivePointerId = ev.getPointerId(pointerIndex);\n                break;\n            }\n            \n            case MotionEvent.ACTION_POINTER_UP:\n                onSecondaryPointerUp(ev);\n                break;\n            \n            case MotionEvent.ACTION_UP: {\n                pointerIndex = ev.findPointerIndex(mActivePointerId);\n                if (pointerIndex < 0) {\n                    GNLog.getInstance().logError(LOG_TAG, \"Got ACTION_UP event but don't have an active pointer id.\");\n                    return false;\n                }\n                \n                if (mIsBeingDragged) {\n                    final float y = ev.getY(pointerIndex);\n                    final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE;\n                    mIsBeingDragged = false;\n                    finishSpinner(overscrollTop);\n                }\n                mActivePointerId = INVALID_POINTER;\n                return false;\n            }\n            case MotionEvent.ACTION_CANCEL:\n                return false;\n        }\n        \n        return true;\n    }\n    \n    private void startDragging(float y) {\n        final float yDiff = y - mInitialDownY;\n        if (yDiff > mTouchSlop && !mIsBeingDragged) {\n            mInitialMotionY = mInitialDownY + mTouchSlop;\n            mIsBeingDragged = true;\n            mProgress.setAlpha(STARTING_PROGRESS_ALPHA);\n        }\n    }\n    \n    private void animateOffsetToCorrectPosition(int from, Animation.AnimationListener listener) {\n        mFrom = from;\n        mAnimateToCorrectPosition.reset();\n        mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION);\n        mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator);\n        if (listener != null) {\n            mCircleView.setAnimationListener(listener);\n        }\n        mCircleView.clearAnimation();\n        mCircleView.startAnimation(mAnimateToCorrectPosition);\n    }\n    \n    private void animateOffsetToStartPosition(int from, Animation.AnimationListener listener) {\n        if (mScale) {\n            // Scale the item back down\n            startScaleDownReturnToStartAnimation(from, listener);\n        } else {\n            mFrom = from;\n            mAnimateToStartPosition.reset();\n            mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION);\n            mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator);\n            if (listener != null) {\n                mCircleView.setAnimationListener(listener);\n            }\n            mCircleView.clearAnimation();\n            mCircleView.startAnimation(mAnimateToStartPosition);\n        }\n    }\n    \n    private final Animation mAnimateToCorrectPosition = new Animation() {\n        @Override\n        public void applyTransformation(float interpolatedTime, Transformation t) {\n            int targetTop = 0;\n            int endTarget = 0;\n            if (!mUsingCustomStart) {\n                endTarget = mSpinnerOffsetEnd - Math.abs(mOriginalOffsetTop);\n            } else {\n                endTarget = mSpinnerOffsetEnd;\n            }\n            targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime));\n            int offset = targetTop - mCircleView.getTop();\n            setTargetOffsetTopAndBottom(offset);\n            mProgress.setArrowScale(1 - interpolatedTime);\n        }\n    };\n    \n    void moveToStart(float interpolatedTime) {\n        int targetTop = 0;\n        targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime));\n        int offset = targetTop - mCircleView.getTop();\n        setTargetOffsetTopAndBottom(offset);\n    }\n    \n    private final Animation mAnimateToStartPosition = new Animation() {\n        @Override\n        public void applyTransformation(float interpolatedTime, Transformation t) {\n            moveToStart(interpolatedTime);\n        }\n    };\n    \n    private void startScaleDownReturnToStartAnimation(int from,\n                                                      Animation.AnimationListener listener) {\n        mFrom = from;\n        mStartingScale = mCircleView.getScaleX();\n        mScaleDownToStartAnimation = new Animation() {\n            @Override\n            public void applyTransformation(float interpolatedTime, Transformation t) {\n                float targetScale = (mStartingScale + (-mStartingScale  * interpolatedTime));\n                setAnimationProgress(targetScale);\n                moveToStart(interpolatedTime);\n            }\n        };\n        mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION);\n        if (listener != null) {\n            mCircleView.setAnimationListener(listener);\n        }\n        mCircleView.clearAnimation();\n        mCircleView.startAnimation(mScaleDownToStartAnimation);\n    }\n    \n    void setTargetOffsetTopAndBottom(int offset) {\n        mCircleView.bringToFront();\n        ViewCompat.offsetTopAndBottom(mCircleView, offset);\n        mCurrentTargetOffsetTop = mCircleView.getTop();\n    }\n    \n    private void onSecondaryPointerUp(MotionEvent ev) {\n        final int pointerIndex = ev.getActionIndex();\n        final int pointerId = ev.getPointerId(pointerIndex);\n        if (pointerId == mActivePointerId) {\n            // This was our active pointer going up. Choose a new\n            // active pointer and adjust accordingly.\n            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;\n            mActivePointerId = ev.getPointerId(newPointerIndex);\n        }\n    }\n    \n    /**\n     * Classes that wish to be notified when the swipe gesture correctly\n     * triggers a refresh should implement this interface.\n     */\n    public interface OnRefreshListener {\n        /**\n         * Called when a swipe gesture triggers a refresh.\n         */\n        void onRefresh();\n    }\n    \n    /**\n     * Classes that wish to override {@link GoNativeSwipeRefreshLayout#canChildScrollUp()} method\n     * behavior should implement this interface.\n     */\n    public interface OnChildScrollUpCallback {\n        /**\n         * Callback that will be called when {@link GoNativeSwipeRefreshLayout#canChildScrollUp()} method\n         * is called to allow the implementer to override its behavior.\n         *\n         * @param parent SwipeRefreshLayout that this callback is overriding.\n         * @param child The child view of SwipeRefreshLayout.\n         *\n         * @return Whether it is possible for the child view of parent layout to scroll up.\n         */\n        boolean canChildScrollUp(@NonNull GoNativeSwipeRefreshLayout parent, @Nullable View child);\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/HandleView.kt",
    "content": "package io.gonative.android.widget\n\nimport android.animation.ArgbEvaluator\nimport android.animation.ValueAnimator\nimport android.content.Context\nimport android.graphics.Color\nimport android.graphics.PorterDuff\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.widget.ImageView\nimport android.widget.RelativeLayout\nimport android.widget.TextView\nimport androidx.annotation.ColorInt\nimport androidx.core.content.res.ResourcesCompat\nimport io.gonative.android.R\n\nclass HandleView : RelativeLayout {\n    private val iconView: ImageView\n    private val textView: TextView\n\n    init {\n        inflate(context, R.layout.view_handle, this)\n        iconView = findViewById(R.id.icon)\n        textView = findViewById(R.id.text)\n    }\n\n    @JvmOverloads\n    constructor(\n        context: Context,\n        attrs: AttributeSet? = null,\n        defStyle: Int = 0\n    ) : super(context, attrs, defStyle) {\n        context.theme.obtainStyledAttributes(attrs, R.styleable.HandleView, 0, 0).apply {\n            val backgroundDrawable = getDrawable(R.styleable.HandleView_handleBackground)\n                ?: ResourcesCompat.getDrawable(\n                    resources,\n                    R.drawable.shape_rounded,\n                    context.theme\n                )\n            val iconDrawable = getDrawable(R.styleable.HandleView_iconDrawable)\n            val text = getString(R.styleable.HandleView_text)\n            val inactiveColor = getColor(R.styleable.HandleView_inactiveColor, inactiveColor)\n            val activeColor = getColor(R.styleable.HandleView_activeColor, activeColor)\n            initView(backgroundDrawable, iconDrawable, text, inactiveColor, activeColor)\n        }\n    }\n\n    constructor(\n        context: Context,\n        backgroundDrawable: Drawable?,\n        iconDrawable: Drawable?,\n        text: String?,\n        @ColorInt inactiveColor: Int,\n        @ColorInt activeColor: Int,\n    ) : super(context, null, 0) {\n        initView(backgroundDrawable, iconDrawable, text, inactiveColor, activeColor)\n    }\n\n    var maxTextWidth: Int = Int.MIN_VALUE\n    var inactiveColor: Int = Color.WHITE\n    var activeColor: Int = Color.WHITE\n\n    fun initView(\n        backgroundDrawable: Drawable?,\n        iconDrawable: Drawable?,\n        text: String?,\n        @ColorInt inactiveColor: Int,\n        @ColorInt activeColor: Int\n    ) {\n        background = backgroundDrawable\n        iconView.setImageDrawable(iconDrawable)\n        setText(text)\n        textView.layoutParams.let {\n            it.width = 0\n            textView.layoutParams = it\n        }\n\n        this.inactiveColor = inactiveColor\n        this.activeColor = activeColor\n        iconView.setColorFilter(inactiveColor)\n    }\n\n    fun setText(text: String?) {\n        textView.layoutParams.width = LayoutParams.WRAP_CONTENT\n        textView.text = text\n        textView.measure(\n            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),\n            MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)\n        )\n        maxTextWidth = textView.measuredWidth\n        textView.layoutParams.let {\n            it.width = 0\n            textView.layoutParams = it\n        }\n    }\n\n    fun animateShowText() {\n        if (textView.text.isEmpty()) {\n            return\n        }\n        if (textView.layoutParams.width != 0) {\n            textView.layoutParams.let {\n                it.width = 0\n                textView.layoutParams = it\n            }\n        }\n        val animator = ValueAnimator.ofInt(0, maxTextWidth)\n        animator.addUpdateListener { anim ->\n            val value = anim.animatedValue as Int\n            textView.layoutParams.let {\n                it.width = value\n                textView.layoutParams = it\n            }\n        }\n        animator.duration = 300\n        animator.start()\n    }\n\n    fun animateHideText() {\n        if (textView.text.isEmpty()) {\n            return\n        }\n        val animator = ValueAnimator.ofInt(maxTextWidth, 0)\n        animator.duration = 300\n        animator.addUpdateListener { anim ->\n            val value = anim.animatedValue as Int\n            val params = textView.layoutParams\n            params.width = value\n            textView.layoutParams = params\n        }\n        animator.start()\n    }\n\n    fun animateActive() {\n        val animator = ValueAnimator.ofObject(ArgbEvaluator(), inactiveColor, activeColor)\n        animator.addUpdateListener { anim ->\n            val color = anim.animatedValue as Int\n            textView.setTextColor(color)\n            iconView.setColorFilter(color, PorterDuff.Mode.SRC_IN)\n        }\n        animator.duration = 100\n        animator.start()\n    }\n\n    fun animateInactive() {\n        val animator = ValueAnimator.ofObject(ArgbEvaluator(), activeColor, inactiveColor)\n        animator.addUpdateListener { anim ->\n            val color = anim.animatedValue as Int\n            textView.setTextColor(color)\n            iconView.setColorFilter(color, PorterDuff.Mode.SRC_IN)\n        }\n        animator.duration = 200\n        animator.start()\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/SwipeHistoryNavigationLayout.kt",
    "content": "package io.gonative.android.widget;\n\nimport android.animation.ObjectAnimator\nimport android.annotation.SuppressLint\nimport android.content.Context\nimport android.graphics.Canvas\nimport android.graphics.drawable.Drawable\nimport android.util.AttributeSet\nimport android.util.DisplayMetrics\nimport android.view.Gravity\nimport android.view.MotionEvent\nimport android.view.View\nimport android.widget.EdgeEffect\nimport android.widget.FrameLayout\nimport androidx.core.content.res.ResourcesCompat\nimport androidx.core.view.ViewCompat\nimport io.gonative.android.R\nimport kotlin.math.abs\nimport kotlin.math.atan2\nimport kotlin.math.max\nimport kotlin.math.min\n\nclass SwipeHistoryNavigationLayout : FrameLayout {\n    private val leftHandleView: HandleView\n    private val rightHandleView: HandleView\n    private val rightEdgeEffect: EdgeEffect\n\n    // Styleable properties\n    private val iconWidth: Float = resources.getDimension(R.dimen.handle_icon_size)\n    private val iconWidthInDp: Float = iconWidth / (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT)\n    private val backgroundDrawable: Drawable?\n    private val leftEdgeDrawable: Drawable?\n    private val rightEdgeDrawable: Drawable?\n    private val firstText: String\n    private val inactiveColor: Int\n    private var activeColor: Int\n    // end Styleable properties\n\n    private var leftHandleFirstPos: Float = Float.NaN\n    private var rightHandleFirstPos: Float = Float.NaN\n\n    /**\n     * Left edge touch detection width.\n     */\n    private var leftEdgeWidth = Float.NaN\n\n    /**\n     * Right edge touch detection width.\n     */\n    private var rightEdgeWidth = Float.NaN\n\n    /**\n     * Swipeable width.\n     */\n    private var swipeableWidth = Float.NaN\n\n    /**\n     * Percentage of screen edges to be judged.\n     */\n    private var edgePer = 5 / 100f\n\n    /**\n     * Ratio of swipeable width to screen width..\n     */\n    private var swipeablePer = 16 / 100f\n\n    /**\n     * Swipe distance threshold before triggering.\n     */\n    private var swipeTriggerThreshold = 80f\n\n    /**\n     * Swipe distance threshold from edge to calculate diagonal motion.\n     */\n    private var swipeEdgeThreshold = 30f\n\n    private var firstTouchX: Int = Int.MIN_VALUE\n    private var isSwipingLeftEdge = false\n    private var isSwipingRightEdge = false\n    private var isTouchInProgress = false\n\n    private var lastTouchX: Float = Float.NaN\n    private var oldDeltaX: Float = Float.NaN\n    private var deltaX: Float = Float.NaN\n    private var isSwipeReachesLimit = false\n\n    private var pointX = 0f\n    private var pointY = 0f\n    private var inMotion = false\n\n    @JvmOverloads\n    constructor(\n        context: Context,\n        attrs: AttributeSet? = null,\n        defStyle: Int = 0\n    ) : super(context, attrs, defStyle) {\n        context.theme.obtainStyledAttributes(attrs, R.styleable.SwipeHistoryNavigationLayout, 0, 0)\n            .apply {\n                backgroundDrawable =\n                    getDrawable(R.styleable.SwipeHistoryNavigationLayout_handleBackground)\n                leftEdgeDrawable =\n                    getDrawable(R.styleable.SwipeHistoryNavigationLayout_leftHandleDrawable)\n                        ?: ResourcesCompat.getDrawable(\n                            resources,\n                            R.drawable.ic_baseline_arrow_back_24,\n                            context.theme\n                        )\n                rightEdgeDrawable =\n                    getDrawable(R.styleable.SwipeHistoryNavigationLayout_rightHandleDrawable)\n                        ?: ResourcesCompat.getDrawable(\n                            resources,\n                            R.drawable.ic_baseline_arrow_forward_24,\n                            context.theme\n                        )\n                firstText =\n                    getString(R.styleable.SwipeHistoryNavigationLayout_leftHandleLabel) ?: \"\"\n                inactiveColor = getColor(\n                    R.styleable.SwipeHistoryNavigationLayout_inactiveColor,\n                    ResourcesCompat.getColor(resources, R.color.swipe_nav_inactive, context.theme)\n                )\n                activeColor = getColor(\n                    R.styleable.SwipeHistoryNavigationLayout_activeColor,\n                    ResourcesCompat.getColor(resources, R.color.swipe_nav_active, context.theme)\n                )\n            }\n\n        leftHandleView = HandleView(\n            context,\n            backgroundDrawable,\n            leftEdgeDrawable,\n            firstText,\n            inactiveColor,\n            activeColor\n        )\n        rightHandleView = HandleView(\n            context,\n            backgroundDrawable,\n            rightEdgeDrawable,\n            \"\",\n            inactiveColor,\n            activeColor\n        )\n        rightEdgeEffect = EdgeEffect(context)\n        setWillNotDraw(false)\n    }\n\n    @SuppressLint(\"RtlHardcoded\")\n    override fun onFinishInflate() {\n        super.onFinishInflate()\n        val leftParams = LayoutParams(\n            LayoutParams.WRAP_CONTENT,\n            LayoutParams.WRAP_CONTENT,\n            Gravity.CENTER_VERTICAL or Gravity.LEFT\n        )\n        addView(leftHandleView, leftParams)\n        addView(\n            rightHandleView, LayoutParams(\n                LayoutParams.WRAP_CONTENT,\n                LayoutParams.WRAP_CONTENT,\n                Gravity.CENTER_VERTICAL\n            )\n        )\n    }\n\n    override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {\n        super.onLayout(changed, left, top, right, bottom)\n        if (changed) {\n            leftHandleView.let {\n                leftHandleFirstPos = -iconWidth\n                it.translationX = leftHandleFirstPos\n            }\n            rightHandleView.let {\n                rightHandleFirstPos = width + iconWidth\n                it.translationX = rightHandleFirstPos\n            }\n\n            leftEdgeWidth = width.toFloat() * edgePer\n            rightEdgeWidth = width - leftEdgeWidth\n            swipeableWidth = width.toFloat() * swipeablePer\n        }\n    }\n\n    override fun isNestedScrollingEnabled(): Boolean {\n        return true\n    }\n\n    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {\n        if (!swipeNavListener.isSwipeEnabled()) {\n            return false\n        }\n\n        when (ev?.action) {\n            MotionEvent.ACTION_DOWN -> {\n                inMotion = false\n                pointX = ev.x\n                pointY = ev.y\n\n                if (isLeftEdge(ev.x) && swipeNavListener.canSwipeLeftEdge()) {\n                    isSwipingLeftEdge = true\n                    firstTouchX = ev.x.toInt()\n                    leftEdgeGrabbed()\n                } else if (isRightEdge(ev.x) && swipeNavListener.canSwipeRightEdge()) {\n                    isSwipingRightEdge = true\n                    firstTouchX = width\n                    rightEdgeGrabbed()\n                }\n            }\n            MotionEvent.ACTION_MOVE -> {\n\n                val diffX = abs(pointX - ev.x)\n                val diffY = abs(pointY - ev.y)\n\n                if (isTouchInProgress) {\n                    return true\n                }\n\n                return if ((isSwipingLeftEdge || isSwipingRightEdge) && ((diffX > swipeEdgeThreshold) || (diffY > swipeEdgeThreshold)) && !inMotion) {\n                    inMotion = true\n                    val angle = atan2(diffY, diffX)\n                    if (angle > Math.PI/6) {\n                        false\n                    } else {\n                        isTouchInProgress = true\n                        parent.requestDisallowInterceptTouchEvent(true)\n                        true\n                    }\n                } else {\n                    false\n                }\n            }\n            MotionEvent.ACTION_UP -> {\n                pointX = 0f\n                pointY = 0f\n                isSwipingLeftEdge = false\n                isSwipingRightEdge = false\n                if (isTouchInProgress) {\n                    return true\n                }\n            }\n        }\n\n        return super.onInterceptTouchEvent(ev)\n    }\n\n    @SuppressLint(\"ClickableViewAccessibility\")\n    override fun onTouchEvent(ev: MotionEvent?): Boolean {\n        var needsInvalidate = false\n        when (ev?.action) {\n            MotionEvent.ACTION_MOVE -> {\n                lastTouchX = ev.x\n                oldDeltaX = deltaX\n                deltaX = abs(lastTouchX - firstTouchX)\n\n                if (isSwipingLeftEdge && swipeNavListener.isSwipeEnabled() && (deltaX >= swipeEdgeThreshold)) {\n                    moveLeftHandle()\n                } else if (isSwipingRightEdge && swipeNavListener.isSwipeEnabled() && (deltaX >= swipeEdgeThreshold)) {\n                    if (swipeNavListener.canSwipeRightEdge()) {\n                        moveRightHandle()\n                    } else if (deltaX > oldDeltaX) {\n                        val over = abs(deltaX - oldDeltaX)\n                        rightEdgeEffect.onPull(over / width)\n                        needsInvalidate = true\n                    }\n                }\n\n                if (deltaX > (swipeableWidth + swipeTriggerThreshold + iconWidthInDp)) {\n                    if (!isSwipeReachesLimit) {\n                        isSwipeReachesLimit = true\n                        swipeReachesLimit()\n                    }\n                } else {\n                    if (isSwipeReachesLimit) {\n                        isSwipeReachesLimit = false\n                        leaveHandle()\n                    }\n                }\n            }\n            MotionEvent.ACTION_UP -> {\n                needsInvalidate = releaseSwipe()\n                parent.requestDisallowInterceptTouchEvent(false)\n            }\n        }\n\n        if (needsInvalidate) {\n            ViewCompat.postInvalidateOnAnimation(this);\n        }\n\n        return super.onTouchEvent(ev)\n    }\n\n\n    private fun isLeftEdge(x: Float) = x <= leftEdgeWidth\n    private fun isRightEdge(x: Float) = x >= rightEdgeWidth\n\n    private fun isTouchedEdge(ev: MotionEvent?): Boolean {\n        // Do not intercept the edges when edge swiping is disabled\n\n        return ev?.action == MotionEvent.ACTION_DOWN && (\n                (isLeftEdge(ev.x) && swipeNavListener.canSwipeLeftEdge())\n                        || (isRightEdge(ev.x) && swipeNavListener.canSwipeRightEdge()))\n                && swipeNavListener.isSwipeEnabled()\n    }\n\n    private fun moveLeftHandle() {\n        leftHandleView.let {\n            val value = (deltaX - swipeEdgeThreshold) - firstTouchX - iconWidth\n            it.translationX = min(value, swipeableWidth - iconWidth)\n        }\n    }\n\n    private fun moveRightHandle() {\n        rightHandleView.let {\n            val value = firstTouchX - (deltaX - swipeEdgeThreshold) + iconWidth / 2\n            it.translationX = max(value, width - swipeableWidth)\n        }\n    }\n\n    private fun leftEdgeGrabbed() {\n        leftHandleView.setText(swipeNavListener.getGoBackLabel())\n    }\n\n    private fun rightEdgeGrabbed() {\n    }\n\n    private fun releaseSwipe(): Boolean {\n        rightEdgeEffect.onRelease()\n\n        if (isSwipingLeftEdge) {\n            if (isSwipeReachesLimit) {\n                leaveHandle()\n                swipeNavListener.navigateBack()\n            }\n            leftHandleView.let {\n                val animator = ObjectAnimator.ofFloat(\n                    it,\n                    View.TRANSLATION_X,\n                    it.translationX,\n                    leftHandleFirstPos\n                )\n                animator.duration = 400\n                animator.start()\n            }\n        } else if (isSwipingRightEdge) {\n            if (isSwipeReachesLimit) {\n                leaveHandle()\n                swipeNavListener.navigateForward()\n            }\n            rightHandleView.let {\n                val animator = ObjectAnimator.ofFloat(\n                    it,\n                    View.TRANSLATION_X,\n                    it.translationX,\n                    rightHandleFirstPos\n                )\n                animator.duration = 400\n                animator.start()\n            }\n        }\n        isSwipingLeftEdge = false\n        isSwipingRightEdge = false\n        isSwipeReachesLimit = false\n        isTouchInProgress = false\n        return rightEdgeEffect.isFinished\n    }\n\n    private fun swipeReachesLimit() {\n        if (isSwipingLeftEdge && swipeNavListener.canSwipeLeftEdge()) {\n            swipeNavListener.leftSwipeReachesLimit()\n            leftHandleView.animateActive()\n            leftHandleView.animateShowText()\n        } else if (isSwipingRightEdge && swipeNavListener.canSwipeRightEdge()) {\n            swipeNavListener.rightSwipeReachesLimit()\n            rightHandleView.animateActive()\n            rightHandleView.animateShowText()\n        }\n    }\n\n    private fun leaveHandle() {\n        if (isSwipingLeftEdge) {\n            leftHandleView.animateInactive()\n            leftHandleView.animateHideText()\n        } else if (isSwipingRightEdge) {\n            rightHandleView.animateInactive()\n            rightHandleView.animateHideText()\n        }\n    }\n\n    fun setActiveColor(color: Int) {\n        activeColor = color;\n        rightHandleView.activeColor = color;\n        leftHandleView.activeColor = color;\n    }\n\n    override fun draw(canvas: Canvas?) {\n        super.draw(canvas)\n        var needsInvalidate = false\n        if (overScrollMode == OVER_SCROLL_ALWAYS || overScrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS) {\n            if (!rightEdgeEffect.isFinished) {\n                canvas?.let {\n                    val restoreCount: Int = canvas.save()\n                    val width: Int = width\n                    val height: Int = height - paddingTop - paddingBottom\n\n                    canvas.rotate(90f)\n                    canvas.translate(paddingTop.toFloat(), -width.toFloat())\n                    rightEdgeEffect.setSize(height, width)\n                    needsInvalidate = needsInvalidate or rightEdgeEffect.draw(canvas)\n                    canvas.restoreToCount(restoreCount)\n                }\n\n            }\n        } else {\n            rightEdgeEffect.finish()\n        }\n        if (needsInvalidate) {\n            // Keep animating\n            ViewCompat.postInvalidateOnAnimation(this);\n        }\n    }\n\n    var swipeNavListener: OnSwipeNavListener = object : OnSwipeNavListener {\n        override fun canSwipeLeftEdge(): Boolean = true\n        override fun canSwipeRightEdge(): Boolean = true\n        override fun getGoBackLabel(): String = \"\"\n        override fun navigateBack(): Boolean = true\n        override fun navigateForward(): Boolean = true\n        override fun leftSwipeReachesLimit() {}\n        override fun rightSwipeReachesLimit() {}\n        override fun isSwipeEnabled(): Boolean = true\n    }\n\n    interface OnSwipeNavListener {\n        /**\n         * Return true if left-edge swipe is to be enabled.\n         */\n        fun canSwipeLeftEdge(): Boolean\n\n        /**\n         * Return true if right-edge swipe is to be enabled.\n         */\n        fun canSwipeRightEdge(): Boolean\n\n        /**\n         * Called when you grab the left edge.\n         * Text to be displayed when swiping to the limit.\n         */\n        fun getGoBackLabel(): String\n\n        /**\n         * Implement the page back operation.\n         */\n        fun navigateBack(): Boolean\n\n        /**\n         * Implement the page forward operation.\n         */\n        fun navigateForward(): Boolean\n\n        /**\n         * Called when the movement of the left-edge swipe reaches its limit.\n         */\n        fun leftSwipeReachesLimit()\n\n        /**\n         * Called when the movement of the right-edge swipe reaches its limit.\n         */\n        fun rightSwipeReachesLimit()\n\n        /**\n         * Return true if swipe edge to navigate is enabled\n         */\n        fun isSwipeEnabled(): Boolean\n    }\n}"
  },
  {
    "path": "app/src/main/java/io/gonative/android/widget/WebViewContainerView.java",
    "content": "package io.gonative.android.widget;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.util.AttributeSet;\nimport android.view.ViewGroup;\nimport android.widget.FrameLayout;\n\nimport androidx.annotation.NonNull;\nimport androidx.annotation.Nullable;\n\nimport java.lang.reflect.Constructor;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\n\nimport io.gonative.android.GoNativeApplication;\nimport io.gonative.android.LeanWebView;\nimport io.gonative.android.MainActivity;\nimport io.gonative.android.WebViewSetup;\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.Bridge;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\npublic class WebViewContainerView extends FrameLayout {\n\n    private ViewGroup webview;\n    private boolean isGecko = false;\n\n    public WebViewContainerView(@NonNull Context context) {\n        super(context);\n    }\n\n    public WebViewContainerView(@NonNull Context context, @Nullable AttributeSet attrs) {\n        super(context, attrs);\n        initializeWebview(context);\n    }\n\n    public WebViewContainerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {\n        super(context, attrs, defStyleAttr);\n        initializeWebview(context);\n    }\n\n    private void initializeWebview(Context context) {\n        AppConfig appConfig = AppConfig.getInstance(context);\n\n        if (appConfig.geckoViewEnabled) {\n            try {\n                Class<?> classGecko = Class.forName(\"io.gonative.plugins.android.gecko.GNGeckoView\");\n                Constructor<?> consGecko = classGecko.getConstructor(Context.class);\n                webview = (ViewGroup) consGecko.newInstance(context);\n                this.isGecko = true;\n            } catch (Exception e) {\n                e.printStackTrace();\n            }\n        } else {\n            webview = new LeanWebView(context);\n        }\n        ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);\n        webview.setLayoutParams(layoutParams);\n        this.addView(webview);\n    }\n\n    public void setupWebview(MainActivity activity, boolean isRoot) {\n        if (isGecko) {\n            try {\n                Class<?> geckoSetupClass = Class.forName(\"io.gonative.plugins.android.gecko.WebViewSetup\");\n                Method setupWebview = geckoSetupClass.getMethod(\"setupWebviewForActivity\", Activity.class, GoNativeWebviewInterface.class, Bridge.class, boolean.class);\n                setupWebview.invoke(geckoSetupClass, activity,  (GoNativeWebviewInterface) webview, ((GoNativeApplication) activity.getApplication()).mBridge, isRoot);\n            } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {\n                e.printStackTrace();\n            }\n        } else {\n            WebViewSetup.setupWebviewForActivity(getWebview(), activity);\n        }\n    }\n\n    public GoNativeWebviewInterface getWebview() {\n        return (GoNativeWebviewInterface) webview;\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/res/anim/fast_fade_out.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<alpha xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:fromAlpha=\"1.0\"\n    android:toAlpha=\"0.0\"\n    android:duration=\"@android:integer/config_shortAnimTime\"\n/>\n"
  },
  {
    "path": "app/src/main/res/drawable/bg_nav_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<shape xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:innerRadius=\"0dp\"\n    android:shape=\"ring\"\n    android:thicknessRatio=\"2\"\n    android:useLevel=\"false\" >\n    <solid android:color=\"#CCEEEEEE\" />\n\n</shape>"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_back_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_baseline_arrow_forward_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"?attr/colorControlNormal\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z\" />\n</vector>"
  },
  {
    "path": "app/src/main/res/drawable/ic_go_back.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#606060\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_go_forward.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#606060\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_stat_onesignal_default.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- OneSignal requires an icon named ic_stat_onesignal_default. -->\n<!-- This aliases ic_notification -->\n<bitmap xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:src=\"@drawable/ic_notification\" />\n"
  },
  {
    "path": "app/src/main/res/drawable/shape_rounded.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<layer-list xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <item\n        android:bottom=\"2dp\"\n        android:left=\"2dp\"\n        android:right=\"2dp\"\n        android:top=\"2dp\">\n        <shape android:shape=\"rectangle\">\n            <corners android:radius=\"@dimen/handle_icon_corner_radius\" />\n            <solid android:color=\"@color/swipe_nav_background\" />\n        </shape>\n    </item>\n</layer-list>"
  },
  {
    "path": "app/src/main/res/layout/actionbar_title.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout\n    xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:orientation=\"horizontal\">\n\n    <androidx.appcompat.widget.LinearLayoutCompat\n        android:id=\"@+id/left_menu_container\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"40dp\"\n        android:gravity=\"center\">\n    </androidx.appcompat.widget.LinearLayoutCompat>\n\n    <RelativeLayout\n        android:id=\"@+id/title_container\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:gravity=\"center_horizontal|center_vertical|center\"\n        android:orientation=\"horizontal\">\n\n    </RelativeLayout>\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_gonative.xml",
    "content": "<io.gonative.android.widget.GoNativeDrawerLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/drawer_layout\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\">\n    <!-- The main content view -->\n    <RelativeLayout\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:orientation=\"vertical\">\n\n        <com.google.android.material.appbar.AppBarLayout\n            android:id=\"@+id/appBar\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\">\n\n            <com.google.android.material.appbar.MaterialToolbar\n                android:id=\"@+id/toolbar\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"?attr/actionBarSize\"\n                app:contentInsetLeft=\"0dp\"\n                app:contentInsetStart=\"0dp\"\n                app:contentInsetStartWithNavigation=\"0dp\"\n                app:elevation=\"80dp\" />\n\n        </com.google.android.material.appbar.AppBarLayout>\n\n        <io.gonative.android.MySwipeRefreshLayout\n            android:id=\"@+id/swipe_refresh\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:layout_above=\"@id/pluginView\"\n            android:layout_below=\"@+id/appBar\">\n\n            <io.gonative.android.widget.SwipeHistoryNavigationLayout\n                android:id=\"@+id/swipe_history_nav\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\">\n\n                <RelativeLayout\n                    android:layout_width=\"fill_parent\"\n                    android:layout_height=\"fill_parent\">\n\n                    <io.gonative.android.widget.WebViewContainerView\n                        android:id=\"@+id/webviewContainer\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\" />\n\n                    <View\n                        android:id=\"@+id/webviewOverlay\"\n                        android:layout_width=\"match_parent\"\n                        android:layout_height=\"match_parent\"\n                        android:alpha=\"0.0\"\n                        android:background=\"?android:attr/colorBackground\" />\n\n                </RelativeLayout>\n            </io.gonative.android.widget.SwipeHistoryNavigationLayout>\n        </io.gonative.android.MySwipeRefreshLayout>\n\n        <RelativeLayout\n            android:id=\"@+id/pluginView\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_above=\"@id/bottom_navigation\"\n            android:layout_alignWithParentIfMissing=\"true\"\n            android:background=\"?android:attr/colorBackground\"\n            android:visibility=\"gone\"/>\n\n        <ProgressBar\n            android:id=\"@+id/progress\"\n            style=\"@android:style/Widget.Holo.Light.ProgressBar.Large\"\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"wrap_content\"\n            android:layout_centerInParent=\"true\"\n            android:scaleX=\"0.6\"\n            android:scaleY=\"0.6\"\n            android:visibility=\"invisible\" />\n\n\n        <com.aurelhubert.ahbottomnavigation.AHBottomNavigation\n            android:id=\"@+id/bottom_navigation\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"wrap_content\"\n            android:layout_alignParentBottom=\"true\"\n            android:elevation=\"8dp\"\n            android:visibility=\"gone\" />\n\n        <RelativeLayout\n            android:id=\"@+id/fullscreen\"\n            android:layout_width=\"match_parent\"\n            android:layout_height=\"match_parent\"\n            android:background=\"?android:attr/colorBackground\"\n            android:layout_above=\"@+id/bottom_navigation\"\n            android:visibility=\"invisible\" />\n\n    </RelativeLayout>\n    <!-- The navigation drawer -->\n    <!-- width should be no more than 320dp -->\n    <com.google.android.material.navigation.NavigationView\n        android:id=\"@+id/left_drawer\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"\n        android:layout_gravity=\"start\"\n        android:layout_weight=\"80\">\n\n        <RelativeLayout\n            android:layout_width=\"wrap_content\"\n            android:layout_height=\"match_parent\"\n            android:paddingTop=\"11dp\">\n\n\n            <Spinner\n                android:id=\"@+id/profile_picker\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginStart=\"5dp\"\n                android:layout_marginLeft=\"5dp\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_marginRight=\"5dp\"\n                android:visibility=\"gone\" />\n\n            <Spinner\n                android:id=\"@+id/segmented_control\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_below=\"@id/profile_picker\"\n                android:layout_marginStart=\"5dp\"\n                android:layout_marginLeft=\"5dp\"\n                android:layout_marginTop=\"10dp\"\n                android:layout_marginEnd=\"5dp\"\n                android:layout_marginRight=\"5dp\"\n                android:visibility=\"gone\" />\n\n            <RelativeLayout\n                android:id=\"@+id/header_layout\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"wrap_content\"\n                android:layout_marginBottom=\"15dp\">\n\n                <ImageView\n                    android:id=\"@+id/app_logo\"\n                    android:layout_width=\"80dp\"\n                    android:layout_height=\"80dp\"\n                    android:layout_centerHorizontal=\"true\"\n                    android:layout_marginTop=\"30dp\"\n                    android:contentDescription=\"@string/app_logo\"\n                    android:src=\"@mipmap/ic_sidebar_logo\" />\n\n                <TextView\n                    android:id=\"@+id/app_name\"\n                    android:layout_width=\"wrap_content\"\n                    android:layout_height=\"wrap_content\"\n                    android:layout_below=\"@+id/app_logo\"\n                    android:layout_centerHorizontal=\"true\"\n                    android:layout_marginTop=\"20dp\"\n                    android:layout_marginBottom=\"15dp\"\n                    android:fontFamily=\"sans-serif-medium\"\n                    android:textColor=\"@color/sidebarHighlight\"\n                    android:textSize=\"22sp\" />\n\n                <View\n                    android:id=\"@+id/header_divider\"\n                    android:layout_width=\"match_parent\"\n                    android:layout_height=\"1dp\"\n                    android:layout_below=\"@id/app_name\"\n                    android:background=\"@color/sidebarSeparatorColor\" />\n            </RelativeLayout>\n\n            <ExpandableListView\n                android:id=\"@+id/drawer_list\"\n                android:layout_width=\"match_parent\"\n                android:layout_height=\"match_parent\"\n                android:layout_below=\"@id/header_layout\"\n                android:layout_marginStart=\"10dp\"\n                android:layout_marginLeft=\"10dp\"\n                android:layout_marginEnd=\"10dp\"\n                android:layout_marginRight=\"10dp\"\n                android:choiceMode=\"singleChoice\"\n                android:divider=\"@null\"\n                android:footerDividersEnabled=\"false\"\n                android:groupIndicator=\"@null\"\n                android:headerDividersEnabled=\"false\"\n                android:listSelector=\"@android:color/transparent\"\n                android:scrollbarStyle=\"insideOverlay\"\n                android:scrollbars=\"vertical\" />\n        </RelativeLayout>\n    </com.google.android.material.navigation.NavigationView>\n</io.gonative.android.widget.GoNativeDrawerLayout>"
  },
  {
    "path": "app/src/main/res/layout/activity_subscriptions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    tools:context=\"io.gonative.android.SubscriptionsActivity\">\n\n    <ProgressBar\n        android:id=\"@+id/progress\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        style=\"@android:style/Widget.Holo.Light.ProgressBar.Large\"\n        android:scaleX=\"0.6\"\n        android:scaleY=\"0.6\"\n        android:layout_centerInParent=\"true\"/>\n    \n    <FrameLayout\n        android:id=\"@+id/subscriptions_fragment\"\n        android:layout_width=\"match_parent\"\n        android:layout_height=\"match_parent\"/>\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/button_menu.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<LinearLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\">\n    <Button\n        android:id=\"@+id/menu_button\"\n        android:layout_width=\"28dp\"\n        android:layout_height=\"28dp\"\n        android:background=\"?selectableItemBackgroundBorderless\"\n        android:paddingStart=\"5dp\"\n        tools:ignore=\"RtlSymmetry\" />\n</LinearLayout>"
  },
  {
    "path": "app/src/main/res/layout/empty.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<FrameLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"0dp\"\n    android:layout_height=\"0dp\">\n</FrameLayout>"
  },
  {
    "path": "app/src/main/res/layout/menu_child_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/menu_item\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\"\n    android:orientation=\"horizontal\">\n\n\n    <ImageView\n        android:id=\"@+id/menu_item_icon\"\n        android:layout_width=\"@dimen/sidebar_icon_size\"\n        android:layout_height=\"@dimen/sidebar_icon_size\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_centerVertical=\"true\"\n        android:layout_marginLeft=\"30dp\"\n        android:layout_marginStart=\"30dp\"\n        android:layout_alignParentStart=\"true\"/>\n\n    <TextView\n        android:id=\"@+id/menu_item_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_toRightOf=\"@id/menu_item_icon\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginStart=\"10dp\"\n        android:paddingTop=\"13dp\"\n        android:paddingBottom=\"13dp\"\n        android:paddingLeft=\"22dp\"\n        android:paddingRight=\"22dp\"\n        android:paddingStart=\"22dp\"\n        android:paddingEnd=\"22dp\"\n        android:fontFamily=\"sans-serif-medium\"\n        android:textColor=\"?android:textColorPrimary\"\n        android:layout_centerVertical=\"true\"\n        android:layout_toEndOf=\"@id/menu_item_icon\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/menu_child_noicon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/menu_item\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/menu_item_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"13dp\"\n        android:paddingBottom=\"13dp\"\n        android:paddingLeft=\"22dp\"\n        android:paddingRight=\"22dp\"\n        android:paddingStart=\"22dp\"\n        android:paddingEnd=\"22dp\"\n        android:fontFamily=\"sans-serif-medium\"\n        android:textColor=\"?android:textColorPrimary\"\n        android:layout_centerVertical=\"true\"/>\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/menu_group_icon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n    android:id=\"@+id/menu_item\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\"\n    android:orientation=\"horizontal\">\n\n\n    <ImageView\n        android:id=\"@+id/menu_item_icon\"\n        android:layout_width=\"@dimen/sidebar_icon_size\"\n        android:layout_height=\"@dimen/sidebar_icon_size\"\n        android:layout_marginLeft=\"10dp\"\n        android:layout_marginStart=\"10dp\"\n        android:layout_marginEnd=\"10dp\"\n        android:layout_marginRight=\"10dp\"\n        android:layout_alignParentLeft=\"true\"\n        android:layout_centerVertical=\"true\"\n        android:layout_alignParentStart=\"true\" />\n\n    <TextView\n        android:id=\"@+id/menu_item_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerVertical=\"true\"\n        android:layout_toEndOf=\"@id/menu_item_icon\"\n        android:layout_toRightOf=\"@id/menu_item_icon\"\n        android:fontFamily=\"sans-serif-medium\"\n        android:paddingStart=\"22dp\"\n        android:paddingLeft=\"22dp\"\n        android:paddingEnd=\"22dp\"\n        android:paddingRight=\"22dp\"\n        android:paddingTop=\"13dp\"\n        android:paddingBottom=\"13dp\"\n        android:textColor=\"?android:textColorPrimary\" />\n\n    <ImageView\n        android:id=\"@+id/menu_group_indicator\"\n        android:layout_width=\"@dimen/sidebar_expand_indicator_size\"\n        android:layout_height=\"@dimen/sidebar_expand_indicator_size\"\n        android:paddingRight=\"10dp\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_centerVertical=\"true\"\n        android:paddingEnd=\"10dp\"\n        android:layout_alignParentEnd=\"true\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/menu_group_noicon.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/menu_item\"\n    android:layout_width=\"fill_parent\"\n    android:layout_height=\"fill_parent\"\n    android:orientation=\"horizontal\">\n\n    <TextView\n        android:id=\"@+id/menu_item_title\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:paddingTop=\"13dp\"\n        android:paddingBottom=\"13dp\"\n        android:paddingLeft=\"22dp\"\n        android:paddingRight=\"22dp\"\n        android:paddingStart=\"22dp\"\n        android:paddingEnd=\"22dp\"\n        android:fontFamily=\"sans-serif-medium\"\n        android:textColor=\"?android:textColorPrimary\"\n        android:layout_centerVertical=\"true\"/>\n\n    <ImageView\n        android:id=\"@+id/menu_group_indicator\"\n        android:layout_width=\"@dimen/sidebar_expand_indicator_size\"\n        android:layout_height=\"@dimen/sidebar_expand_indicator_size\"\n        android:paddingRight=\"10dp\"\n        android:layout_alignParentRight=\"true\"\n        android:layout_centerVertical=\"true\"\n        android:layout_alignParentEnd=\"true\"\n        android:paddingEnd=\"10dp\" />\n\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/layout/profile_picker_dropdown.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"?android:attr/listPreferredItemHeightSmall\"\n    android:gravity=\"center_vertical\"\n    android:padding=\"5dp\"\n    android:textAppearance=\"?android:attr/textAppearanceMedium\"\n    android:textColor=\"?android:textColorPrimary\">\n</TextView>"
  },
  {
    "path": "app/src/main/res/layout/splash_screen.xml",
    "content": "<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:background=\"@drawable/splash\">\n\n    <TextView\n        android:id=\"@+id/banner_text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_alignParentBottom=\"true\"\n        android:layout_centerHorizontal=\"true\"\n        android:layout_marginBottom=\"60dp\"\n        android:gravity=\"center_horizontal\"\n        android:text=\"@string/banner_message\"\n        android:textColor=\"@color/colorPrimary\"\n        android:visibility=\"invisible\"\n        android:textSize=\"18sp\" />\n\n</RelativeLayout>\n"
  },
  {
    "path": "app/src/main/res/layout/tab.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n\n<TextView xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:id=\"@+id/tab_title\"\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n    android:gravity=\"center\"\n    android:ellipsize=\"none\"\n    android:singleLine=\"true\" />"
  },
  {
    "path": "app/src/main/res/layout/view_handle.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<RelativeLayout xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\n    android:layout_width=\"wrap_content\"\n    android:layout_height=\"wrap_content\"\n    android:background=\"@drawable/shape_rounded\"\n    android:elevation=\"@dimen/handle_icon_elevation\"\n    android:orientation=\"horizontal\"\n    tools:layout_marginLeft=\"10dp\"\n    tools:layout_marginTop=\"10dp\"\n    tools:targetApi=\"lollipop\">\n\n    <ImageView\n        android:id=\"@+id/icon\"\n        android:layout_width=\"@dimen/handle_icon_size\"\n        android:layout_height=\"@dimen/handle_icon_size\"\n        android:layout_alignParentStart=\"true\"\n        android:padding=\"@dimen/handle_icon_padding\"\n        android:src=\"@drawable/ic_baseline_arrow_back_24\" />\n\n    <TextView\n        android:id=\"@+id/text\"\n        android:layout_width=\"wrap_content\"\n        android:layout_height=\"wrap_content\"\n        android:layout_centerVertical=\"true\"\n        android:layout_toEndOf=\"@id/icon\"\n        android:lines=\"1\"\n        android:paddingRight=\"@dimen/handle_text_padding_right\"\n        android:paddingBottom=\"@dimen/handle_text_padding_bottom\"\n        android:textColor=\"@color/swipe_nav_inactive\"\n        tools:text=\"@string/close_to_app\" />\n</RelativeLayout>"
  },
  {
    "path": "app/src/main/res/menu/topmenu.xml",
    "content": "<menu xmlns:android=\"http://schemas.android.com/apk/res/android\"\r\n    xmlns:app=\"http://schemas.android.com/apk/res-auto\">\r\n</menu>\r\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@color/ic_launcher_background\"/>\n    <foreground android:drawable=\"@mipmap/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/res/values/attrs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <declare-styleable name=\"PagerSlidingTabStrip\">\n        <attr name=\"pstsForegroundColor\" format=\"color\" />\n        <attr name=\"pstsIndicatorColor\" format=\"color\" />\n        <attr name=\"pstsUnderlineColor\" format=\"color\" />\n        <attr name=\"pstsDividerColor\" format=\"color\" />\n        <attr name=\"pstsDividerWidth\" format=\"dimension\" />\n        <attr name=\"pstsIndicatorHeight\" format=\"dimension\" />\n        <attr name=\"pstsUnderlineHeight\" format=\"dimension\" />\n        <attr name=\"pstsDividerPadding\" format=\"dimension\" />\n        <attr name=\"pstsTabPaddingLeftRight\" format=\"dimension\" />\n        <attr name=\"pstsScrollOffset\" format=\"dimension\" />\n        <attr name=\"pstsTabBackground\" format=\"reference\" />\n        <attr name=\"pstsShouldExpand\" format=\"boolean\" />\n        <attr name=\"pstsTextAllCaps\" format=\"boolean\" />\n        <attr name=\"pstsPaddingMiddle\" format=\"boolean\" />\n        <attr name=\"pstsTextStyle\">\n            <flag name=\"normal\" value=\"0x0\" />\n            <flag name=\"bold\" value=\"0x1\" />\n            <flag name=\"italic\" value=\"0x2\" />\n        </attr>\n        <attr name=\"pstsTextSelectedStyle\">\n            <flag name=\"normal\" value=\"0x0\" />\n            <flag name=\"bold\" value=\"0x1\" />\n            <flag name=\"italic\" value=\"0x2\" />\n        </attr>\n        <attr name=\"pstsTextAlpha\" format=\"float\" />\n        <attr name=\"pstsTextSelectedAlpha\" format=\"float\" />\n    </declare-styleable>\n\n    <bool name=\"isTablet\">false</bool>\n\n</resources>"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#ffffff</color>\n    <color name=\"colorPrimaryDark\">#757575</color>\n    <color name=\"titleTextColor\">#000000</color>\n\n    <color name=\"colorAccent\">#1E496E</color>\n    <color name=\"colorBackground\">#ffffff</color>\n    <color name=\"drawerArrow\">#000000</color>\n\n    <color name=\"sidebarForeground\">#1e496e</color>\n    <color name=\"sidebarBackground\">#ffffff</color>\n    <color name=\"sidebarSeparatorColor\">#30808080</color>\n    <color name=\"sidebarHighlight\">#1e496e</color>\n\n    <color name=\"tabBarBackground\">#ffffff</color>\n    <color name=\"tabBarTextColor\">#949494</color>\n    <color name=\"tabBarIndicator\">#1e496e</color>\n\n    <color name=\"swipe_nav_background\">#fafafa</color>\n    <color name=\"swipe_nav_inactive\">#888888</color>\n    <color name=\"swipe_nav_active\">#1e88e5</color>\n\n    <color name=\"pull_to_refresh_color\">#1e496e</color>\n\n    <color name=\"splash_background\">#FFFFFF</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values/dimens.xml",
    "content": "<resources>\n\n    <!-- Default screen margins, per the Android Design guidelines. -->\n    <dimen name=\"activity_horizontal_margin\">16dp</dimen>\n    <dimen name=\"activity_vertical_margin\">16dp</dimen>\n    <dimen name=\"sidebar_icon_size\">22dp</dimen>\n    <dimen name=\"sidebar_expand_indicator_size\">22dp</dimen>\n    <dimen name=\"sidebar_icon_width\">25dp</dimen>\n    <dimen name=\"bottom_navigation_margin_bottom\">7dp</dimen>\n    <dimen name=\"bottom_navigation_margin_top_active\">7dp</dimen>\n    <dimen name=\"bottom_navigation_margin_top_inactive\">7dp</dimen>\n\n    <dimen name=\"handle_icon_corner_radius\">32dp</dimen>\n    <dimen name=\"handle_icon_size\">38dp</dimen>\n    <dimen name=\"handle_icon_padding\">8dp</dimen>\n    <dimen name=\"handle_icon_elevation\">2dp</dimen>\n    <dimen name=\"handle_text_padding_right\">12dp</dimen>\n    <dimen name=\"handle_text_padding_bottom\">1dp</dimen>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?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",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<resources>\r\n    <integer name=\"round_corner_dips\">15</integer>\r\n    <integer name=\"tabbar_icon_size\">20</integer>\r\n    <integer name=\"tabbar_icon_padding\">2</integer>\r\n    <integer name=\"sidebar_icon_size\">22</integer>\r\n    <integer name=\"sidebar_expand_indicator_size\">22</integer>\r\n    <integer name=\"action_button_size\">48</integer>\r\n</resources>\r\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n\t<string name=\"app_name\">GoNative</string>\n\t<string name=\"action_search\">Search</string>\n\t<string name=\"action_refresh\">Refresh</string>\n    <string name=\"action_share\">Share</string>\n\t<string name=\"shake_to_clear_cache\">Shake Device to Delete Cache</string>\n\t<string-array name=\"device_shaken_options\">\n\t\t<item>Delete Cache</item>\n\t\t<item>Cancel</item>\n\t</string-array>\n\t<string name=\"drawer_open\">Open drawer</string>\n\t<string name=\"drawer_close\">Close drawer</string>\n\t<string name=\"search_hint\">Search</string>\n    <string name=\"form_error\">Problem with submission. Check inputs and try again.</string>\n    <string name=\"download\">Download</string>\n    <string name=\"download_canceled\">Download canceled</string>\n\t<string name=\"download_channel_name\">File downloads</string>\n\t<string name=\"download_channel_description\">Notifications for file downloads</string>\n\t<string name=\"download_in_progress\">Download in progress</string>\n\t<string name=\"download_complete\">Download complete</string>\n\t<string name=\"download_disconnected\">Download connection failed.</string>\n\t<string name=\"upload_camera_permission_denied\">Camera permission denied</string>\n\t<string name=\"file_handler_not_found\">Could not find an app to open this file.</string>\n\t<string name=\"cannot_open_file_chooser\">Could not open file chooser to handle this file type.</string>\n\t<string name=\"external_storage_explanation\">Allow photo access to upload file.</string>\n\t<string name=\"ssl_error_generic\">Error establishing SSL Connection</string>\n\t<string name=\"ssl_error_expired\">SSL certificate has expired</string>\n\t<string name=\"ssl_error_cert\">Invalid SSL certificate</string>\n    <string name=\"location_services_not_enabled\">To continue, turn on device location, which uses Google\\'s location service</string>\n    <string name=\"ok\">OK</string>\n    <string name=\"no_thanks\">No Thanks</string>\n\t<string name=\"request_permission_explanation_geolocation\">Please allow location permission</string>\n\t<string name=\"request_permission_explanation_storage\">Please allow storage permission for file download</string>\n\t<string name=\"starting_download\">Starting download</string>\n\t<string name=\"subscriptions_activity_title\">Subscriptions</string>\n\t<string name=\"cleared_cache\">Cleared cache</string>\n\t<string name=\"app_logo\">App Logo</string>\n\t<string name=\"google_service_required\">Google Service JSON was not properly set up.</string>\n\t<string name=\"banner_message\">Powered by GoNative</string>\n\t<string name=\"choose_action\">Choose an action</string>\n    <string name=\"app_not_installed\">App is not installed</string>\n\t<string name=\"file_download_title\">Download started</string>\n\t<string name=\"file_download_finished\">File downloaded</string>\n\t<string name=\"file_download_finished_with_name\">%1$s downloaded</string>\n\t<string name=\"file_download_error\">Download Error</string>\n\t<string name=\"file_download_finished_gallery\">Image saved to Gallery</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"GoNativeTheme.NoActionBar\" parent=\"GN.DayNight.NoActionBar\">\n        <!--Remove action bar shadow. We are managing it ourselves with the tabs.-->\n        <item name=\"android:windowContentOverlay\">@null</item>\n\n        <item name=\"drawerArrowStyle\">@style/DrawerArrowStyle</item>\n\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"titleTextColor\">@color/titleTextColor</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n        <item name=\"android:colorBackground\">@color/colorBackground</item>\n    </style>\n\n    <style name=\"DrawerArrowStyle\" parent=\"@style/Widget.AppCompat.DrawerArrowToggle\">\n        <item name=\"spinBars\">true</item>\n        <item name=\"color\">@color/drawerArrow</item>\n    </style>\n\n    <attr name=\"ic_action_refresh\" format=\"reference\"/>\n    <attr name=\"ic_action_search\" format=\"reference\"/>\n    <attr name=\"ic_action_share\" format=\"reference\"/>\n\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n        <!-- Can add custom styles -->\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_black_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_black_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_black_24dp</item>\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true</item>\n    </style>\n\n    <style name=\"GN.DayNight.NoActionBar\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <!-- Can add custom styles -->\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_black_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_black_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_black_24dp</item>\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">true</item>\n    </style>\n\n    <style name=\"LoginFormContainer\">\n        <item name=\"android:layout_width\">match_parent</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:padding\">16dp</item>\n    </style>\n\n    <style name=\"SplashScreen\" parent=\"Theme.AppCompat.NoActionBar\">\n    </style>\n\n    <style name=\"SplashScreenAnimation\">\n        <item name=\"android:windowExitAnimation\">@anim/fast_fade_out</item>\n    </style>\n\n    <style name=\"SplashTheme\" parent=\"Theme.SplashScreen\">\n        <item name=\"android:windowBackground\">@color/splash_background</item>\n        <item name=\"windowSplashScreenAnimationDuration\">0</item>\n        <item name=\"postSplashScreenTheme\">@style/SplashScreen</item>\n        <item name=\"windowSplashScreenAnimatedIcon\">@android:color/transparent</item>\n    </style>\n\n    <attr name=\"inactiveColor\" format=\"color\" />\n    <attr name=\"activeColor\" format=\"color\" />\n    <attr name=\"handleBackground\" format=\"reference\" />\n\n    <declare-styleable name=\"HandleView\">\n        <attr name=\"iconDrawable\" format=\"reference\" />\n        <attr name=\"text\" format=\"string\" />\n        <attr name=\"inactiveColor\" />\n        <attr name=\"activeColor\" />\n        <attr name=\"handleBackground\" />\n    </declare-styleable>\n\n    <declare-styleable name=\"SwipeHistoryNavigationLayout\">\n        <attr name=\"leftHandleDrawable\" format=\"reference\" />\n        <attr name=\"rightHandleDrawable\" format=\"reference\" />\n        <attr name=\"handleBackground\" />\n        <attr name=\"leftHandleLabel\" format=\"string\" />\n        <attr name=\"inactiveColor\" />\n        <attr name=\"activeColor\" />\n    </declare-styleable>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-ko/strings.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <string name=\"shake_to_clear_cache\">기기를 흔들면 캐시를 삭제할 수 있음</string>\n    <string-array name=\"device_shaken_options\">\n        <item>캐시 삭제</item>\n        <item>취소</item>\n    </string-array>\n    <string name=\"cleared_cache\">캐시 삭제중</string>\n    <string name=\"choose_action\">작업 선택</string>\n    <string name=\"file_download_title\">다운로드 시작됨</string>\n    <string name=\"file_download_finished\">파일 다운로드 완료</string>\n    <string name=\"file_download_finished_with_name\">다운로드한 파일: %1$s</string>\n    <string name=\"download_canceled\">다운로드 취소됨</string>\n    <string name=\"file_download_error\">다운로드 오류</string>\n    <string name=\"file_download_finished_gallery\">갤러리에 저장된 이미지</string>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-large/styles.xml",
    "content": "<resources>\n\n    <style name=\"LoginFormContainer\">\n        <item name=\"android:layout_width\">400dp</item>\n        <item name=\"android:layout_height\">wrap_content</item>\n        <item name=\"android:layout_gravity\">center</item>\n        <item name=\"android:padding\">16dp</item>\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"colorPrimary\">#212121</color>\n    <color name=\"colorPrimaryDark\">#030303</color>\n    <color name=\"titleTextColor\">#ffffff</color>\n\n    <color name=\"colorAccent\">#80cbc4</color>\n    <color name=\"colorBackground\">#444444</color>\n    <color name=\"drawerArrow\">#ffffff</color>\n\n    <color name=\"sidebarForeground\">#ffffff</color>\n    <color name=\"sidebarBackground\">#333333</color>\n    <color name=\"sidebarSeparatorColor\">#30ffffff</color>\n    <color name=\"sidebarHighlight\">#ffffff</color>\n\n    <color name=\"tabBarBackground\">#333333</color>\n    <color name=\"tabBarTextColor\">#ffffff</color>\n    <color name=\"tabBarIndicator\">#666666</color>\n\n    <color name=\"swipe_nav_background\">#666666</color>\n    <color name=\"swipe_nav_inactive\">#888888</color>\n    <color name=\"swipe_nav_active\">#ffffff</color>\n\n    <color name=\"pull_to_refresh_color\">#ffffff</color>\n\n    <color name=\"splash_background\">#1e496e</color>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-night/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources xmlns:tools=\"http://schemas.android.com/tools\">\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">false</item>\n    </style>\n\n    <style name=\"GN.DayNight.NoActionBar\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:windowLightStatusBar\" tools:targetApi=\"m\">false</item>\n    </style>\n\n    <style name=\"SplashScreen\" parent=\"Theme.AppCompat.NoActionBar\">\n    </style>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-night-v29/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n        <!-- Can add custom styles -->\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:forceDarkAllowed\">true</item>\n        <item name=\"android:isLightTheme\">false</item>\n    </style>\n\n    <style name=\"GN.DayNight.NoActionBar\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:forceDarkAllowed\">true</item>\n        <item name=\"android:isLightTheme\">false</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/values-sw600dp/attr.xml",
    "content": "<?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",
    "content": "<resources>\r\n\r\n    <!--\r\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         screen margins) for sw600dp devices (e.g. 7\" tablets) here.\r\n    -->\r\n\r\n</resources>\r\n"
  },
  {
    "path": "app/src/main/res/values-sw720dp-land/dimens.xml",
    "content": "<resources>\r\n\r\n    <!--\r\n         Customize dimensions originally defined in res/values/dimens.xml (such as\n         screen margins) for sw720dp devices (e.g. 10\" tablets) in landscape here.\r\n    -->\r\n    <dimen name=\"activity_horizontal_margin\">128dp</dimen>\r\n\r\n</resources>\r\n"
  },
  {
    "path": "app/src/main/res/values-v21/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.LightWithDarkActionBar\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:spotShadowAlpha\">1</item>\n    </style>\n\n    <style name=\"GN.LightWithNoActionBar\" parent=\"Theme.AppCompat.NoActionBar\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:spotShadowAlpha\">1</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values-v29/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"GN.DayNight\" parent=\"Theme.AppCompat.DayNight\">\n        <!-- Can add custom styles -->\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_black_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_black_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_black_24dp</item>\n        <item name=\"android:forceDarkAllowed\">true</item>\n        <item name=\"android:isLightTheme\">true</item>\n    </style>\n\n    <style name=\"GN.DayNight.NoActionBar\" parent=\"Theme.AppCompat.DayNight.NoActionBar\">\n        <item name=\"ic_action_refresh\">@drawable/ic_refresh_white_24dp</item>\n        <item name=\"ic_action_search\">@drawable/ic_search_white_24dp</item>\n        <item name=\"ic_action_share\">@drawable/ic_share_white_24dp</item>\n        <item name=\"android:forceDarkAllowed\">true</item>\n        <item name=\"android:isLightTheme\">true</item>\n    </style>\n</resources>"
  },
  {
    "path": "app/src/main/res/xml/filepaths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\n    <cache-path path=\"downloads\" name=\"downloads\" />\n    <external-path name=\"external_download\" path=\"Download\"/>\n    <external-files-path name=\"files_pictures\" path=\"Pictures\"/>\n    <external-path name=\"external_files\" path=\".\"/>\n    <files-path name=\"internal_files\" path=\".\" />\n</paths>"
  },
  {
    "path": "app/src/main/res/xml/network_security_config.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<network-security-config>\n    <base-config cleartextTrafficPermitted=\"true\">\n        <trust-anchors>\n            <certificates src=\"system\" />\n        </trust-anchors>\n    </base-config>\n</network-security-config>"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/GoNativeWebChromeClient.java",
    "content": "package io.gonative.android;\n\nimport android.Manifest;\nimport android.annotation.TargetApi;\nimport android.content.pm.PackageManager;\nimport android.net.Uri;\nimport android.os.Message;\nimport android.os.SystemClock;\nimport android.util.Log;\nimport android.view.View;\nimport android.view.ViewGroup;\nimport android.webkit.ConsoleMessage;\nimport android.webkit.GeolocationPermissions;\nimport android.webkit.JsResult;\nimport android.webkit.PermissionRequest;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebView;\nimport android.widget.RelativeLayout;\n\nimport androidx.appcompat.app.AlertDialog;\n\nimport java.util.ArrayList;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\n\n/**\n* Created by weiyin on 2/2/15.\n* Copyright 2014 GoNative.io LLC\n*/\nclass GoNativeWebChromeClient extends WebChromeClient {\n    private final MainActivity mainActivity;\n    private final UrlNavigation urlNavigation;\n    private final boolean webviewConsoleLogEnabled;\n    private View customView;\n    private CustomViewCallback callback;\n    private boolean isFullScreen = false;\n    private long deniedGeolocationUptime;\n\n    public GoNativeWebChromeClient(MainActivity mainActivity, UrlNavigation urlNavigation) {\n        this.mainActivity = mainActivity;\n        this.urlNavigation = urlNavigation;\n        this.deniedGeolocationUptime = 0;\n        this.webviewConsoleLogEnabled = AppConfig.getInstance(mainActivity).enableWebConsoleLogs;\n        if (this.webviewConsoleLogEnabled) {\n            Log.d(\"GoNative WebView\", \"Web Console logs enabled\");\n        }\n    }\n\n    @Override\n    public boolean onJsAlert(WebView view, String url, String message, JsResult result){\n        new AlertDialog.Builder(mainActivity)\n                .setMessage(message)\n                .setPositiveButton(R.string.ok, (dialog, which) -> result.confirm())\n                .setOnDismissListener(dialog -> result.cancel()).show();\n        return true;\n    }\n\n    @Override\n    public boolean onJsBeforeUnload(WebView view, String url, String message, JsResult result) {\n        urlNavigation.cancelLoadTimeout();\n        return super.onJsBeforeUnload(view, url, message, result);\n    }\n\n    @Override\n    public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) {\n        if (!AppConfig.getInstance(mainActivity).usesGeolocation) {\n            callback.invoke(origin, false, false);\n            return;\n        }\n\n        // There is a bug in Android webview where this function will be continuously called in\n        // a loop if we run callback.invoke asynchronously with granted=false, degrading webview\n        // and javascript performance. If we have recently been denied geolocation by the user,\n        // run callback.invoke(granted=false) synchronously and do not prompt user.\n        //\n        // Note: this infinite loop situation also happens if we run callback.invoke(origin, true, false),\n        // regardless if we do it synchronously or async.\n        long elapsed = SystemClock.uptimeMillis() - deniedGeolocationUptime;\n        if (elapsed < 1000 /* 1 second */) {\n            callback.invoke(origin, false, false);\n            return;\n        }\n\n        mainActivity.getRuntimeGeolocationPermission(new MainActivity.GeolocationPermissionCallback() {\n            @Override\n            public void onResult(boolean granted) {\n                // only retain if granted\n                callback.invoke(origin, granted, granted);\n                if (!granted) {\n                    deniedGeolocationUptime = SystemClock.uptimeMillis();\n                }\n            }\n        });\n    }\n\n    @Override\n    public void onShowCustomView(View view, CustomViewCallback callback) {\n        RelativeLayout fullScreen = this.mainActivity.getFullScreenLayout();\n        if (fullScreen == null) return;\n\n        this.customView = view;\n        this.callback = callback;\n        this.isFullScreen = true;\n\n        fullScreen.setVisibility(View.VISIBLE);\n        fullScreen.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,\n                ViewGroup.LayoutParams.MATCH_PARENT));\n\n        this.mainActivity.toggleFullscreen(this.isFullScreen);\n    }\n\n    @Override\n    public void onHideCustomView() {\n        this.customView = null;\n        this.isFullScreen = false;\n\n        RelativeLayout fullScreen = this.mainActivity.getFullScreenLayout();\n        if (fullScreen != null) {\n            fullScreen.setVisibility(View.INVISIBLE);\n            fullScreen.removeAllViews();\n        }\n\n        if (this.callback != null) {\n            callback.onCustomViewHidden();\n        }\n\n        this.mainActivity.toggleFullscreen(this.isFullScreen);\n    }\n\n    public boolean exitFullScreen() {\n        if (this.isFullScreen) {\n            onHideCustomView();\n            return true;\n        } else {\n            return false;\n        }\n    }\n\n    @Override\n    public void onCloseWindow(WebView window) {\n        if (mainActivity.isNotRoot()) mainActivity.finish();\n    }\n\n    @Override\n    public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {\n        // make sure there is no existing message\n        mainActivity.cancelFileUpload();\n\n        boolean multiple = false;\n        switch (fileChooserParams.getMode()) {\n            case FileChooserParams.MODE_OPEN:\n                multiple = false;\n                break;\n            case FileChooserParams.MODE_OPEN_MULTIPLE:\n                multiple = true;\n                break;\n            case FileChooserParams.MODE_SAVE:\n            default:\n                // MODE_SAVE is unimplemented\n                filePathCallback.onReceiveValue(null);\n                return false;\n        }\n\n        mainActivity.setUploadMessageLP(filePathCallback);\n    \n        // Checks web's input file params for \"capture\" attribute\n        if (fileChooserParams.isCaptureEnabled()) {\n            return urlNavigation.openDirectCamera(fileChooserParams.getAcceptTypes(), multiple);\n        }\n    \n        return urlNavigation.chooseFileUpload(fileChooserParams.getAcceptTypes(), multiple);\n    }\n\n    // For Android > 4.1\n    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {\n        // make sure there is no existing message\n        mainActivity.cancelFileUpload();\n\n        mainActivity.setUploadMessage(uploadMsg);\n        if (acceptType == null) acceptType = \"*/*\";\n        urlNavigation.chooseFileUpload(new String[]{acceptType});\n    }\n\n    // Android 3.0 +\n    public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {\n        openFileChooser(uploadMsg, acceptType, null);\n    }\n\n    //Android 3.0\n    public void openFileChooser(ValueCallback<Uri> uploadMsg) {\n        openFileChooser(uploadMsg, null, null);\n    }\n\n    @Override\n    public void onReceivedTitle(WebView view, String title){\n        mainActivity.updatePageTitle();\n    }\n\n    @Override\n    public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {\n        urlNavigation.createNewWindow(view, resultMsg);\n        return true;\n    }\n\n    @Override\n    @TargetApi(21)\n    public void onPermissionRequest(final PermissionRequest request) {\n        String[] resources = request.getResources();\n\n        ArrayList<String> permissions = new ArrayList<>();\n        for (int i = 0; i < resources.length; i++) {\n            if (resources[i].equals(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {\n                permissions.add(Manifest.permission.RECORD_AUDIO);\n                permissions.add(Manifest.permission.MODIFY_AUDIO_SETTINGS);\n            } else if (resources[i].equals(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {\n                permissions.add(Manifest.permission.CAMERA);\n            }\n        }\n\n        String[] permissionsArray = new String[permissions.size()];\n        permissionsArray = permissions.toArray(permissionsArray);\n\n        mainActivity.getPermission(permissionsArray, new MainActivity.PermissionCallback() {\n            @Override\n            public void onPermissionResult(String[] permissions, int[] grantResults) {\n                ArrayList<String> grantedPermissions = new ArrayList<String>();\n                for (int i = 0; i < grantResults.length; i++) {\n                    if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {\n                        continue;\n                    }\n\n                    if (permissions[i].equals(Manifest.permission.RECORD_AUDIO)) {\n                        grantedPermissions.add(PermissionRequest.RESOURCE_AUDIO_CAPTURE);\n                    } else if (permissions[i].equals(Manifest.permission.CAMERA)) {\n                        grantedPermissions.add(PermissionRequest.RESOURCE_VIDEO_CAPTURE);\n                    }\n                }\n\n                if (grantedPermissions.isEmpty()) {\n                    request.deny();\n                } else {\n                    String[] grantedPermissionsArray = new String[grantedPermissions.size()];\n                    grantedPermissionsArray = grantedPermissions.toArray(grantedPermissionsArray);\n                    request.grant(grantedPermissionsArray);\n                }\n            }\n        });\n    }\n\n    @Override\n    public void onPermissionRequestCanceled(PermissionRequest request) {\n        super.onPermissionRequestCanceled(request);\n    }\n\n    @Override\n    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {\n        if (webviewConsoleLogEnabled) {\n            switch (consoleMessage.messageLevel()) {\n                case LOG:\n                    Log.i(\"[console.log]\", consoleMessage.message());\n                    break;\n                case DEBUG:\n                case TIP:\n                    Log.d(\"[console.debug]\", consoleMessage.message());\n                    break;\n                case WARNING:\n                    Log.w(\"[console.warn]\", consoleMessage.message());\n                    break;\n                case ERROR:\n                    GNLog.getInstance().logError(\"[console.error]\", consoleMessage.message(), new Exception(consoleMessage.message()), GNLog.TYPE_WEB_CONSOLE);\n                    break;\n            }\n        }\n        return true;\n    }\n}\n"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/GoNativeWebviewClient.java",
    "content": "package io.gonative.android;\n\nimport android.annotation.TargetApi;\nimport android.content.Context;\nimport android.graphics.Bitmap;\nimport android.net.Uri;\nimport android.net.http.SslError;\nimport android.os.Build;\nimport android.os.Message;\nimport android.webkit.ClientCertRequest;\nimport android.webkit.SslErrorHandler;\nimport android.webkit.WebResourceError;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport androidx.annotation.RequiresApi;\n\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Created by weiyin on 9/9/15.\n */\npublic class GoNativeWebviewClient extends WebViewClient{\n    private static final String TAG = GoNativeWebviewClient.class.getName();\n    private UrlNavigation urlNavigation;\n    private Context context;\n\n    public GoNativeWebviewClient(MainActivity mainActivity, UrlNavigation urlNavigation) {\n        this.urlNavigation = urlNavigation;\n        this.context = mainActivity;\n    }\n\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, String url) {\n        return urlNavigation.shouldOverrideUrlLoading((GoNativeWebviewInterface)view, url);\n    }\n\n    public boolean shouldOverrideUrlLoading(WebView view, String url, boolean isReload) {\n        return urlNavigation.shouldOverrideUrlLoading((GoNativeWebviewInterface)view, url, isReload, false);\n    }\n\n    @Override\n    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {\n            Uri uri = request.getUrl();\n            return urlNavigation.shouldOverrideUrlLoading((GoNativeWebviewInterface)view, uri.toString(), false, request.isRedirect());\n        }\n        return super.shouldOverrideUrlLoading(view, request);\n    }\n\n    @Override\n    public void onPageStarted(WebView view, String url, Bitmap favicon) {\n        super.onPageStarted(view, url, favicon);\n\n        urlNavigation.onPageStarted(url);\n    }\n\n    @Override\n    public void onPageFinished(WebView view, String url) {\n        super.onPageFinished(view, url);\n\n        urlNavigation.onPageFinished((GoNativeWebviewInterface)view, url);\n    }\n\n    @Override\n    public void onFormResubmission(WebView view, Message dontResend, Message resend) {\n        urlNavigation.onFormResubmission((GoNativeWebviewInterface)view, dontResend, resend);\n    }\n\n    @Override\n    public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) {\n        urlNavigation.doUpdateVisitedHistory((GoNativeWebviewInterface)view, url, isReload);\n    }\n\n    @Override\n    public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {\n        urlNavigation.onReceivedError((GoNativeWebviewInterface) view, errorCode, description, failingUrl);\n    }\n\n    @TargetApi(Build.VERSION_CODES.M)\n    @Override\n    public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {\n        urlNavigation.onReceivedError((GoNativeWebviewInterface) view, error.getErrorCode(),\n                error.getDescription().toString(), request.getUrl().toString());\n    }\n\n    @Override\n    public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {\n        handler.cancel();\n        urlNavigation.onReceivedSslError(error, view.getUrl());\n    }\n\n    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)\n    @Override\n    public void onReceivedClientCertRequest(WebView view, ClientCertRequest request) {\n        urlNavigation.onReceivedClientCertRequest(view.getUrl(), request);\n    }\n\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {\n        return urlNavigation.interceptHtml((LeanWebView)view, url);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {\n\n        WebResourceResponse wr = ((GoNativeApplication) context.getApplicationContext()).mBridge.interceptHtml((MainActivity) context, request);\n        if (wr != null) {\n            return wr;\n        }\n\n        String method = request.getMethod();\n        if (method == null || !method.equalsIgnoreCase(\"GET\")) return null;\n\n        android.net.Uri uri = request.getUrl();\n        if (uri == null || !uri.getScheme().startsWith(\"http\")) return null;\n\n        return shouldInterceptRequest(view, uri.toString());\n    }\n}\n"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/LeanWebView.java",
    "content": "package io.gonative.android;\n\nimport android.app.Activity;\nimport android.content.Context;\nimport android.os.Build;\nimport android.os.Bundle;\nimport android.util.AttributeSet;\nimport android.util.DisplayMetrics;\nimport android.util.JsonReader;\nimport android.util.JsonToken;\nimport android.view.GestureDetector;\nimport android.view.MotionEvent;\nimport android.webkit.ValueCallback;\nimport android.webkit.WebBackForwardList;\nimport android.webkit.WebChromeClient;\nimport android.webkit.WebHistoryItem;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport java.io.IOException;\nimport java.io.StringReader;\n\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Pass calls WebViewClient.shouldOverrideUrlLoading when loadUrl, reload, or goBack are called.\n */\npublic class LeanWebView extends WebView implements GoNativeWebviewInterface {\n    private WebViewClient mClient = null;\n    private WebChromeClient mChromeClient = null;\n    private boolean checkLoginSignup = true;\n    private GestureDetector gd;\n    private OnSwipeListener onSwipeListener;\n    private boolean zoomed = false;\n\n    public LeanWebView(Context context) {\n        super(context);\n        gd = new GestureDetector(context, sogl);\n    }\n\n    public LeanWebView(Context context, AttributeSet attrs) {\n        super(context, attrs);\n        gd = new GestureDetector(context, sogl);\n    }\n\n    public LeanWebView(Context context, AttributeSet attrs, int defStyle) {\n        super(context, attrs, defStyle);\n        gd = new GestureDetector(context, sogl);\n    }\n\n    GestureDetector.SimpleOnGestureListener sogl = new GestureDetector.SimpleOnGestureListener() {\n        @Override\n        public boolean onFling(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {\n            if (onSwipeListener == null) return false;\n            return compareEvents(event1, event2, velocityX, velocityY);\n        }\n    \n        @Override\n        public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX, float distanceY) {\n            if (onSwipeListener == null) return false;\n            return compareEvents(event1, event2, 0, 0);\n        }\n        \n        private int getScreenX(){\n            DisplayMetrics displayMetrics = new DisplayMetrics();\n            ((Activity)getContext()).getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);\n            return displayMetrics.widthPixels;\n        }\n\n        @Override\n        public boolean onDown(MotionEvent e) {\n            return true;\n        }\n    \n        private boolean compareEvents(MotionEvent event1, MotionEvent event2, float velocityX, float velocityY) {\n            int swipeVelocityThreshold = 0;\n            int swipeThreshold = 100;\n            int edgeArea = 500;\n            int diagonalMovementThreshold = 100;\n        \n            float diffY = event2.getY() - event1.getY();\n            float diffX = event2.getX() - event1.getX();\n            if (Math.abs(diffX) > (Math.abs(diffY) - diagonalMovementThreshold)\n                    && Math.abs(velocityX) > swipeVelocityThreshold\n                    && Math.abs(diffX) > swipeThreshold) {\n                if (diffX > 0 && event1.getX() < edgeArea) {\n                    // swipe from edge\n                    onSwipeListener.onSwipeRight();\n                } else if (event1.getX() > getScreenX() - edgeArea) {\n                    // swipe from edge\n                    onSwipeListener.onSwipeLeft();\n                }\n                return true;\n            }\n            return false;\n        }\n    };\n\n    @Override\n    public boolean onTouchEvent(MotionEvent event) {\n        gd.onTouchEvent(event);\n        return super.onTouchEvent(event);\n    }\n\n    @Override\n    public void setWebViewClient(WebViewClient client) {\n        mClient = client;\n        super.setWebViewClient(client);\n    }\n\n    @Override\n    public void setWebChromeClient(WebChromeClient client) {\n        mChromeClient = client;\n        super.setWebChromeClient(client);\n    }\n\n    @Override\n    public void loadUrl(String url) {\n        if (url == null) return;\n        if (UrlNavigation.OFFLINE_PAGE_URL_RAW.equals(url)) {\n            url = UrlNavigation.OFFLINE_PAGE_URL;\n        }\n\n        if (url.startsWith(\"javascript:\"))\n            runJavascript(url.substring(\"javascript:\".length()));\n        else if (mClient == null || !mClient.shouldOverrideUrlLoading(this, url)) {\n            super.loadUrl(url);\n        }\n    }\n\n    @Override\n    public void reload() {\n        if (mClient == null || !(mClient instanceof GoNativeWebviewClient)) super.reload();\n        else if(!((GoNativeWebviewClient)mClient).shouldOverrideUrlLoading(this, getUrl(), true))\n            super.reload();\n    }\n\n    @Override\n    public void goBack() {\n        try {\n            WebBackForwardList history = copyBackForwardList();\n            // find first non-offline item\n            WebHistoryItem item = null;\n            int steps = 0;\n            for (int i = history.getCurrentIndex() - 1; i >= 0; i--) {\n                WebHistoryItem temp = history.getItemAtIndex(i);\n\n                if (!temp.getUrl().equals(UrlNavigation.OFFLINE_PAGE_URL)) {\n                    item = temp;\n                    steps = i - history.getCurrentIndex();\n                    break;\n                }\n            }\n\n            if (item == null) return;\n\n            // this shouldn't be necessary, but sometimes we are not able to get an updated\n            // intercept url from onPageStarted, so this call to shouldOverrideUrlLoading ensures\n            // that our html interceptor knows about this url.\n            if (mClient.shouldOverrideUrlLoading(this, item.getUrl())) {\n                return;\n            }\n            super.goBackOrForward(steps);\n        } catch (Exception ignored) {\n            super.goBack();\n        }\n    }\n\n    @Override\n    public boolean canGoForward() {\n        WebBackForwardList history = copyBackForwardList();\n\n        // Checks if the next forward item is the offline page\n        WebHistoryItem item = history.getItemAtIndex(history.getCurrentIndex() + 1);\n        if (item != null && UrlNavigation.OFFLINE_PAGE_URL.equals(item.getUrl())) {\n            // return true if the item after the offline page is not null, otherwise, return false\n            WebHistoryItem itemAfterOfflinePage = history.getItemAtIndex(history.getCurrentIndex() + 2);\n            return itemAfterOfflinePage != null;\n        }\n\n        return super.canGoForward();\n    }\n\n    @Override\n    public void goForward() {\n        WebBackForwardList history = copyBackForwardList();\n\n        // Checks if the next forward item is the offline page\n        WebHistoryItem item = history.getItemAtIndex(history.getCurrentIndex() + 1);\n        if (item != null && UrlNavigation.OFFLINE_PAGE_URL.equals(item.getUrl())) {\n            // If item next to the offline page is not null, load the item\n            WebHistoryItem itemAfterOfflinePage = history.getItemAtIndex(history.getCurrentIndex() + 2);\n            if (itemAfterOfflinePage != null) {\n                goBackOrForward(2);\n            }\n            return;\n        }\n\n        super.goForward();\n    }\n\n    private boolean urlEqualsIgnoreSlash(String url1, String url2) {\n        if (url1 == null || url2 == null) return false;\n        if (url1.endsWith(\"/\")) {\n            url1 = url1.substring(0, url1.length() - 1);\n        }\n        if (url2.endsWith(\"/\")) {\n            url2 = url2.substring(0, url2.length() - 1);\n        }\n        return url1.equals(url2);\n    }\n\n    // skip shouldOverrideUrlLoading, including its html override logic.\n    public void loadUrlDirect(String url) {\n        super.loadUrl(url);\n    }\n\n    public boolean checkLoginSignup() {\n        return checkLoginSignup;\n    }\n\n    public void setCheckLoginSignup(boolean checkLoginSignup) {\n        this.checkLoginSignup = checkLoginSignup;\n    }\n\n    public void runJavascript(String js) {\n        // before Kitkat, the only way to run javascript was to load a url that starts with \"javascript:\".\n        // Starting in Kitkat, the \"javascript:\" method still works, but it expects the rest of the string\n        // to be URL encoded, unlike previous versions. Rather than URL encode for Kitkat and above,\n        // use the new evaluateJavascript method.\n        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {\n            loadUrlDirect(\"javascript:\" + js);\n        } else {\n            evaluateJavascript(js, new ValueCallback<String>() {\n                @Override\n                public void onReceiveValue(String value) {\n                    JsonReader reader = new JsonReader(new StringReader(value));\n                    // Must set lenient to parse single values\n                    reader.setLenient(true);\n                    try {\n                        if(reader.peek() != JsonToken.NULL) {\n                            if(reader.peek() == JsonToken.STRING) {\n                                String result = reader.nextString();\n                                if(result != null) {\n                                    JsResultBridge.jsResult = result;\n                                }\n                            } else {\n                                JsResultBridge.jsResult = value;\n                            }\n                        }\n                    } catch (IOException e) {\n                        JsResultBridge.jsResult = \"GoNativeGetJsResultsError\";\n                    }\n                }\n            });\n        }\n    }\n\n    public boolean exitFullScreen() {\n        if (mChromeClient != null && mChromeClient instanceof GoNativeWebChromeClient) {\n            return ((GoNativeWebChromeClient) mChromeClient).exitFullScreen();\n        } else {\n            return false;\n        }\n    }\n\n    static public boolean isCrosswalk() {\n        return false;\n    }\n\n    @Override\n    public void saveStateToBundle(Bundle outBundle) {\n        saveState(outBundle);\n    }\n\n    @Override\n    public void restoreStateFromBundle(Bundle inBundle) {\n        restoreState(inBundle);\n    }\n\n    @Override\n    public int getWebViewScrollY() {\n        return getScrollY();\n    }\n\n    @Override\n    public int getWebViewScrollX() {\n        return getScrollX();\n    }\n\n    @Override\n    public void scrollTo(int scrollX, int scrollY) {\n        super.scrollTo(scrollX, scrollY);\n    }\n\n    public interface OnSwipeListener {\n        void onSwipeLeft();\n        void onSwipeRight();\n    }\n    \n    /**\n     * @deprecated use GoNativeEdgeSwipeLayout in place of LeanWebView's swipe listener.\n     */\n    public OnSwipeListener getOnSwipeListener() {\n        return onSwipeListener;\n    }\n    \n    /**\n     * @deprecated use GoNativeEdgeSwipeLayout in place of LeanWebView's swipe listener.\n     */\n    public void setOnSwipeListener(OnSwipeListener onSwipeListener) {\n        this.onSwipeListener = onSwipeListener;\n    }\n    \n    @Override\n    public int getMaxHorizontalScroll() {\n        return computeHorizontalScrollRange() - getWidth();\n    }\n    \n    @Override\n    public void zoomBy(float zoom){\n        super.zoomBy(zoom);\n        zoomed = true;\n    }\n    \n    @Override\n    public boolean isZoomed(){\n        return zoomed;\n    }\n    \n    @Override\n    public boolean zoomOut(){\n        zoomed = false;\n        return super.zoomOut();\n    }\n}\n"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/PoolWebViewClient.java",
    "content": "package io.gonative.android;\n\nimport android.annotation.TargetApi;\nimport android.os.Build;\nimport android.os.Handler;\nimport android.webkit.WebResourceRequest;\nimport android.webkit.WebResourceResponse;\nimport android.webkit.WebView;\nimport android.webkit.WebViewClient;\n\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Created by weiyin on 9/9/15.\n */\npublic class PoolWebViewClient extends WebViewClient {\n    private WebViewPool.WebViewPoolCallback webViewPoolCallback;\n\n    public PoolWebViewClient(WebViewPool.WebViewPoolCallback webViewPoolCallback, LeanWebView view) {\n        this.webViewPoolCallback = webViewPoolCallback;\n        view.setWebViewClient(this);\n    }\n\n    @Override\n    public void onPageFinished(final WebView view, String url) {\n        super.onPageFinished(view, url);\n\n        // remove self as webviewclient\n        new Handler(view.getContext().getMainLooper()).post(new Runnable() {\n            @Override\n            public void run() {\n                view.setWebViewClient(null);\n            }\n        });\n\n        webViewPoolCallback.onPageFinished((GoNativeWebviewInterface) view, url);\n    }\n\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {\n        return webViewPoolCallback.interceptHtml((GoNativeWebviewInterface)view, url);\n    }\n\n    @TargetApi(Build.VERSION_CODES.LOLLIPOP)\n    @Override\n    public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {\n        String method = request.getMethod();\n        if (method == null || !method.equalsIgnoreCase(\"GET\")) return null;\n\n        android.net.Uri uri = request.getUrl();\n        if (uri == null || !uri.getScheme().startsWith(\"http\")) return null;\n\n        return shouldInterceptRequest(view, uri.toString());\n    }\n}\n"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/WebViewSetup.java",
    "content": "package io.gonative.android;\n\nimport android.annotation.SuppressLint;\nimport android.content.Context;\nimport android.os.Build;\nimport android.os.Message;\nimport android.webkit.CookieManager;\nimport android.webkit.WebSettings;\nimport android.webkit.WebView;\n\nimport java.util.Map;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.GNLog;\nimport io.gonative.gonative_core.GoNativeWebviewInterface;\n\n/**\n * Created by weiyin on 9/8/15.\n */\npublic class WebViewSetup {\n    private static final String TAG = WebViewSetup.class.getName();\n\n    @SuppressLint(\"JavascriptInterface\")\n    public static void setupWebviewForActivity(GoNativeWebviewInterface webview, MainActivity activity) {\n        if (!(webview instanceof LeanWebView)) {\n            GNLog.getInstance().logError(TAG, \"Expected webview to be of class LeanWebView and not \" + webview.getClass().getName());\n            return;\n        }\n\n        LeanWebView wv = (LeanWebView)webview;\n\n        setupWebview(wv, activity);\n\n        UrlNavigation urlNavigation = new UrlNavigation(activity);\n        urlNavigation.setCurrentWebviewUrl(webview.getUrl());\n\n        wv.setWebChromeClient(new GoNativeWebChromeClient(activity, urlNavigation));\n        wv.setWebViewClient(new GoNativeWebviewClient(activity, urlNavigation));\n\n        FileDownloader fileDownloader = activity.getFileDownloader();\n        if (fileDownloader != null) {\n            wv.setDownloadListener(fileDownloader);\n            fileDownloader.setUrlNavigation(urlNavigation);\n        }\n\n        ProfilePicker profilePicker = activity.getProfilePicker();\n        wv.removeJavascriptInterface(\"gonative_profile_picker\");\n        if (profilePicker != null) {\n            wv.addJavascriptInterface(profilePicker.getProfileJsBridge(), \"gonative_profile_picker\");\n        }\n\n        wv.removeJavascriptInterface(\"gonative_status_checker\");\n        wv.addJavascriptInterface(activity.getStatusCheckerBridge(), \"gonative_status_checker\");\n\n        wv.removeJavascriptInterface(\"gonative_file_writer_sharer\");\n        wv.addJavascriptInterface(activity.getFileWriterSharer().getJavascriptBridge(), \"gonative_file_writer_sharer\");\n\n        wv.removeJavascriptInterface(\"JSBridge\");\n        wv.addJavascriptInterface(activity.getJavascriptBridge(), \"JSBridge\");\n\n        ((GoNativeApplication) activity.getApplication()).mBridge.onWebviewSetUp(activity, wv);\n\n        if (activity.getIntent().getBooleanExtra(MainActivity.EXTRA_WEBVIEW_WINDOW_OPEN, false)) {\n            // send to other webview\n            Message resultMsg = ((GoNativeApplication)activity.getApplication()).getWebviewMessage();\n            if (resultMsg != null) {\n                WebView.WebViewTransport transport = (WebView.WebViewTransport)resultMsg.obj;\n                if (transport != null) {\n                    transport.setWebView(wv);\n                    resultMsg.sendToTarget();\n                }\n            }\n        }\n    }\n\n    @SuppressWarnings(\"deprecation\")\n    @SuppressLint(\"SetJavaScriptEnabled\")\n    public static void setupWebview(GoNativeWebviewInterface webview, Context context) {\n        if (!(webview instanceof LeanWebView)) {\n            GNLog.getInstance().logError(TAG, \"Expected webview to be of class LeanWebView and not \" + webview.getClass().getName());\n            return;\n        }\n\n        AppConfig appConfig = AppConfig.getInstance(context);\n\n        LeanWebView wv = (LeanWebView)webview;\n        WebSettings webSettings = wv.getSettings();\n\n        if (AppConfig.getInstance(context).allowZoom) {\n            webSettings.setBuiltInZoomControls(true);\n        }\n        else {\n            webSettings.setBuiltInZoomControls(false);\n        }\n\n        webSettings.setDisplayZoomControls(false);\n        webSettings.setLoadWithOverviewMode(true);\n        webSettings.setUseWideViewPort(true);\n\n        webSettings.setJavaScriptEnabled(true);\n        webSettings.setJavaScriptCanOpenWindowsAutomatically(true);\n\n        // font size bug fix, see https://stackoverflow.com/questions/41179357/android-webview-rem-units-scale-way-to-large-for-boxes\n        webSettings.setMinimumFontSize(1);\n        webSettings.setMinimumLogicalFontSize(1);\n\n        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {\n            webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);\n            CookieManager.getInstance().setAcceptThirdPartyCookies(wv, true);\n        }\n\n        webSettings.setDomStorageEnabled(true);\n        webSettings.setCacheMode(appConfig.cacheMode.webSettingsCacheMode());\n\n        webSettings.setDatabaseEnabled(true);\n\n        webSettings.setSaveFormData(false);\n        webSettings.setSavePassword(false);\n        webSettings.setUserAgentString(appConfig.userAgent);\n        webSettings.setSupportMultipleWindows(appConfig.enableWindowOpen);\n        webSettings.setGeolocationEnabled(appConfig.usesGeolocation);\n        webSettings.setMediaPlaybackRequiresUserGesture(false);\n\n        if (appConfig.webviewTextZoom > 0) {\n            webSettings.setTextZoom(appConfig.webviewTextZoom);\n        }\n    }\n\n    public static void setupWebviewGlobals(Context context) {\n        // WebView debugging\n        if(!AppConfig.getInstance(context).geckoViewEnabled) {\n            Map<String,Object> installation = Installation.getInfo(context);\n            String dist = (String)installation.get(\"distribution\");\n            if (dist != null && (dist.equals(\"debug\") || dist.equals(\"adhoc\"))) {\n                WebView.setWebContentsDebuggingEnabled(true);\n            }\n        }\n    }\n\n    public static void removeCallbacks(LeanWebView webview) {\n        webview.setWebViewClient(null);\n        webview.setWebChromeClient(null);\n    }\n}\n"
  },
  {
    "path": "app/src/normal/java/io/gonative/android/WebkitCookieManagerProxy.java",
    "content": "package io.gonative.android;\n\nimport java.io.IOException;\nimport java.net.CookiePolicy;\nimport java.net.CookieStore;\nimport java.net.HttpCookie;\nimport java.net.URI;\nimport java.util.Arrays;\nimport java.util.Calendar;\nimport java.util.Date;\nimport java.util.List;\nimport java.util.Map;\n\nimport io.gonative.gonative_core.AppConfig;\nimport io.gonative.gonative_core.LeanUtils;\n\n// this syncs cookies between webkit (webview) and java.net classes\npublic class WebkitCookieManagerProxy extends java.net.CookieManager {\n    private static final String TAG = WebkitCookieManagerProxy.class.getName();\n\tprivate android.webkit.CookieManager webkitCookieManager;\n\n    public WebkitCookieManagerProxy()\n    {\n        this(null, null);\n    }\n    \n    WebkitCookieManagerProxy(CookieStore store, CookiePolicy cookiePolicy)\n    {\n        super(null, cookiePolicy);\n\n        this.webkitCookieManager = android.webkit.CookieManager.getInstance();\n    }\n\n    // java.net.CookieManager overrides\n    @Override\n    public void put(URI uri, Map<String, List<String>> responseHeaders) throws IOException \n    {\n        // make sure our args are valid\n        if ((uri == null) || (responseHeaders == null)) return;\n\n        // save our url once\n        String url = uri.toString();\n\n        String expiryString = null;\n        int sessionExpiry = AppConfig.getInstance(null).forceSessionCookieExpiry;\n\n        // go over the headers\n        for (String headerKey : responseHeaders.keySet()) \n        {\n            // ignore headers which aren't cookie related\n            if ((headerKey == null) || !headerKey.equalsIgnoreCase(\"Set-Cookie\")) continue;\n\n            // process each of the headers\n            for (String headerValue : responseHeaders.get(headerKey))\n            {\n                boolean passOriginalHeader = true;\n                if (sessionExpiry > 0) {\n                    List<HttpCookie> cookies = HttpCookie.parse(headerValue);\n                    for (HttpCookie cookie : cookies) {\n                        if (cookie.getMaxAge() < 0 || cookie.getDiscard()) {\n                            // this is a session cookie. Modify it and pass it to the webview.\n                            cookie.setMaxAge(sessionExpiry);\n                            cookie.setDiscard(false);\n                            if (expiryString == null) {\n                                Calendar calendar = Calendar.getInstance();\n                                calendar.add(Calendar.SECOND, sessionExpiry);\n                                Date expiryDate = calendar.getTime();\n                                expiryString = \"; expires=\" + LeanUtils.formatDateForCookie(expiryDate) +\n                                        \"; Max-Age=\" + Integer.toString(sessionExpiry);\n                            }\n\n                            StringBuilder newHeader = new StringBuilder();\n                            newHeader.append(cookie.toString());\n                            newHeader.append(expiryString);\n                            if (cookie.getPath() != null) {\n                                newHeader.append(\"; path=\");\n                                newHeader.append(cookie.getPath());\n                            }\n                            if (cookie.getDomain() != null) {\n                                newHeader.append(\"; domain=\");\n                                newHeader.append(cookie.getDomain());\n                            }\n                            if (cookie.getSecure()) {\n                                newHeader.append(\"; secure\");\n                            }\n\n                            this.webkitCookieManager.setCookie(url, newHeader.toString());\n                            passOriginalHeader = false;\n                        }\n                    }\n                }\n\n                if (passOriginalHeader) this.webkitCookieManager.setCookie(url, headerValue);\n            }\n        }\n    }\n\n    @Override\n    public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) throws IOException \n    {\n        // make sure our args are valid\n        if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException(\"Argument is null\");\n\n        // save our url once\n        String url = uri.toString();\n\n        // prepare our response\n        Map<String, List<String>> res = new java.util.HashMap<String, List<String>>();\n\n        // get the cookie\n        String cookie = this.webkitCookieManager.getCookie(url);\n\n        // return it\n        if (cookie != null) res.put(\"Cookie\", Arrays.asList(cookie));\n        return res;\n    }\n\n    @Override\n    public CookieStore getCookieStore() \n    {\n        // we don't want anyone to work with this cookie store directly\n        throw new UnsupportedOperationException();\n    }    \n    \n}\n"
  },
  {
    "path": "build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\nbuildscript {\n    ext {\n        kotlin_version = '1.8.21'\n        coreVersion = '1.5.32'\n        iconsVersion = '1.2.0'\n    }\n    repositories {\n        google()\n        mavenCentral()\n        gradlePluginPortal()\n        jcenter()\n        maven {\n            url \"https://plugins.gradle.org/m2/\"\n        }\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:7.4.2'\n        classpath 'gradle.plugin.com.onesignal:onesignal-gradle-plugin:0.14.0'\n        classpath \"org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version\"\n        //[enabled by builder] classpath 'com.google.gms:google-services:4.3.13'\n        //[enabled by builder] classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7'\n    }\n}\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n        maven { url 'https://jitpack.io' }\n        jcenter()\n        maven { url(\"$rootDir/maven\") }\n    }\n}\n"
  },
  {
    "path": "generate-app-icons.sh",
    "content": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\nsips -z 1024 1024 -s format png --out $BASEDIR/AppIconTemp.png $BASEDIR/AppIcon 2>&1\n\n# create icon surrounded by transparent border (AppIconBordered). Make border slightly less than 256 so background does not bleed through\nconvert $BASEDIR/AppIconTemp.png -bordercolor transparent -border 250 $BASEDIR/AppIconBordered.png\n\n# create rounded rectangle icon (AppIconRound)\nconvert -size 1024x1024 xc:none -draw \"roundrectangle 0,0,1024,1024,80,80\" $BASEDIR/mask.png 2>&1\nconvert $BASEDIR/AppIconTemp.png -matte $BASEDIR/mask.png -compose DstIn -composite $BASEDIR/AppIconRound.png 2>&1\n\nrm -f $BASEDIR/AppIconTemp.png\nrm -f $BASEDIR/mask.png\n\nsips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/mipmap-mdpi/ic_launcher.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/mipmap-hdpi/ic_launcher.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_launcher.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_launcher.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 192 192 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png $BASEDIR/AppIconRound.png 2>&1\n\nsips -z 108 108 -s format png --out $BASEDIR/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png $BASEDIR/AppIconBordered.png 2>&1\nsips -z 162 162 -s format png --out $BASEDIR/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png $BASEDIR/AppIconBordered.png 2>&1\nsips -z 216 216 -s format png --out $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png $BASEDIR/AppIconBordered.png 2>&1\nsips -z 324 324 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png $BASEDIR/AppIconBordered.png 2>&1\nsips -z 432 432 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png $BASEDIR/AppIconBordered.png 2>&1\n\n\noptipng $BASEDIR/app/src/main/res/mipmap-mdpi/ic_launcher.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-hdpi/ic_launcher.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_launcher.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_launcher.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png 2>&1\n\n# sidebar logo\nSIDEBAR_LOGO=$BASEDIR/SidebarLogo\nif [[ -f \"SIDEBAR_LOGO\" ]]; then\n  sips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/mipmap-mdpi/ic_sidebar_logo.png $BASEDIR/SidebarLogo 2>&1\n  sips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/mipmap-hdpi/ic_sidebar_logo.png $BASEDIR/SidebarLogo 2>&1\n  sips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_sidebar_logo.png $BASEDIR/SidebarLogo 2>&1\n  sips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_sidebar_logo.png $BASEDIR/SidebarLogo 2>&1\n  sips -z 192 192 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_sidebar_logo.png $BASEDIR/SidebarLogo 2>&1\nelse\n  sips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/mipmap-mdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/mipmap-hdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 192 192 -s format png --out $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\nfi\n\noptipng $BASEDIR/app/src/main/res/mipmap-mdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-hdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xhdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxhdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-xxxhdpi/ic_sidebar_logo.png 2>&1\n\nSIDEBAR_LOG_DARK=$BASEDIR/SideBarLogoDark\nif [[ -f \"SIDEBAR_LOG_DARK\" ]]; then\n  sips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-mdpi/ic_sidebar_logo.png $BASEDIR/SideBarLogoDark 2>&1\n  sips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-hdpi/ic_sidebar_logo.png $BASEDIR/SideBarLogoDark 2>&1\n  sips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xhdpi/ic_sidebar_logo.png $BASEDIR/SideBarLogoDark 2>&1\n  sips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xxhdpi/ic_sidebar_logo.png $BASEDIR/SideBarLogoDark 2>&1\n  sips -z 192 192 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xxxhdpi/ic_sidebar_logo.png $BASEDIR/SideBarLogoDark 2>&1\nelse\n  sips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-mdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-hdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xxhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\n  sips -z 192 192 -s format png --out $BASEDIR/app/src/main/res/mipmap-night-xxxhdpi/ic_sidebar_logo.png $BASEDIR/AppIconRound.png 2>&1\nfi\n\noptipng $BASEDIR/app/src/main/res/mipmap-night-mdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-night-hdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-night-xhdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-night-xxhdpi/ic_sidebar_logo.png 2>&1\noptipng $BASEDIR/app/src/main/res/mipmap-night-xxxhdpi/ic_sidebar_logo.png 2>&1\n\nsips -z 36 36 -s format png --out $BASEDIR/app/src/main/res/drawable-mdpi/ic_actionbar.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 54 54 -s format png --out $BASEDIR/app/src/main/res/drawable-hdpi/ic_actionbar.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/drawable-xhdpi/ic_actionbar.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 108 108 -s format png --out $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_actionbar.png $BASEDIR/AppIconRound.png 2>&1\nsips -z 144 144 -s format png --out $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_actionbar.png $BASEDIR/AppIconRound.png 2>&1\n\noptipng $BASEDIR/app/src/main/res/drawable-mdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-hdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_actionbar.png 2>&1\n\nrm -rf $BASEDIR/AppIconRound.png\nrm -rf $BASEDIR/AppIconBordered.png\n\n# notification icon\nsips -z 24 24 -s format png --out $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png $BASEDIR/NotificationIcon 2>&1\nsips -z 36 36 -s format png --out $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png $BASEDIR/NotificationIcon 2>&1\nsips -z 48 48 -s format png --out $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png $BASEDIR/NotificationIcon 2>&1\nsips -z 72 72 -s format png --out $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png $BASEDIR/NotificationIcon 2>&1\nsips -z 96 96 -s format png --out $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png $BASEDIR/NotificationIcon 2>&1\n\noptipng $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png 2>&1"
  },
  {
    "path": "generate-header-images.sh",
    "content": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\n\nsips --resampleHeight 36 -s format png --out $BASEDIR/app/src/main/res/drawable-mdpi/ic_actionbar.png $BASEDIR/HeaderImage 2>&1\nsips --resampleHeight 54 -s format png --out $BASEDIR/app/src/main/res/drawable-hdpi/ic_actionbar.png $BASEDIR/HeaderImage 2>&1\nsips --resampleHeight 72 -s format png --out $BASEDIR/app/src/main/res/drawable-xhdpi/ic_actionbar.png $BASEDIR/HeaderImage 2>&1\nsips --resampleHeight 108 -s format png --out $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_actionbar.png $BASEDIR/HeaderImage 2>&1\nsips --resampleHeight 144 -s format png --out $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_actionbar.png $BASEDIR/HeaderImage 2>&1\n\noptipng $BASEDIR/app/src/main/res/drawable-mdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-hdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_actionbar.png 2>&1\n\n# dark theme icons\nDARK_ICON=$BASEDIR/HeaderImageDark\nif [[ -f \"$DARK_ICON\" ]]; then\nsips --resampleHeight 36 -s format png --out $BASEDIR/app/src/main/res/drawable-night-mdpi/ic_actionbar.png $BASEDIR/HeaderImageDark 2>&1\nsips --resampleHeight 54 -s format png --out $BASEDIR/app/src/main/res/drawable-night-hdpi/ic_actionbar.png $BASEDIR/HeaderImageDark 2>&1\nsips --resampleHeight 72 -s format png --out $BASEDIR/app/src/main/res/drawable-night-xhdpi/ic_actionbar.png $BASEDIR/HeaderImageDark 2>&1\nsips --resampleHeight 108 -s format png --out $BASEDIR/app/src/main/res/drawable-night-xxhdpi/ic_actionbar.png $BASEDIR/HeaderImageDark 2>&1\nsips --resampleHeight 144 -s format png --out $BASEDIR/app/src/main/res/drawable-night-xxxhdpi/ic_actionbar.png $BASEDIR/HeaderImageDark 2>&1\n\noptipng $BASEDIR/app/src/main/res/drawable-night-mdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-night-hdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-night-xhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-night-xxhdpi/ic_actionbar.png 2>&1\noptipng $BASEDIR/app/src/main/res/drawable-night-xxxhdpi/ic_actionbar.png 2>&1\nfi"
  },
  {
    "path": "generate-plugin-icons.sh",
    "content": "#!/bin/sh\n\nBASEDIR=$(dirname $0)\n\n# intercom notification icon\nif [[ $1 == \"intercom\" ]]\nthen\n  cp $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png $BASEDIR/app/src/main/res/drawable-mdpi/intercom_push_icon.png\n  cp $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png $BASEDIR/app/src/main/res/drawable-hdpi/intercom_push_icon.png\n  cp $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png $BASEDIR/app/src/main/res/drawable-xhdpi/intercom_push_icon.png\n  cp $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png $BASEDIR/app/src/main/res/drawable-xxhdpi/intercom_push_icon.png\n  cp $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png $BASEDIR/app/src/main/res/drawable-xxxhdpi/intercom_push_icon.png\nfi\n\n# cordial notification icon\nif [[ $1 == \"cordial\" ]]\nthen\n  cp $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png $BASEDIR/plugins/cordial/src/main/res/drawable-mdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png $BASEDIR/plugins/cordial/src/main/res/drawable-hdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png $BASEDIR/plugins/cordial/src/main/res/drawable-xhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png $BASEDIR/plugins/cordial/src/main/res/drawable-xxhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png $BASEDIR/plugins/cordial/src/main/res/drawable-xxxhdpi/ic_notification.png\nfi\n\n# braze notification icon\nif [[ $1 == \"braze\" ]]\nthen\n  cp $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png $BASEDIR/plugins/braze/src/main/res/drawable-mdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png $BASEDIR/plugins/braze/src/main/res/drawable-hdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png $BASEDIR/plugins/braze/src/main/res/drawable-xhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png $BASEDIR/plugins/braze/src/main/res/drawable-xxhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png $BASEDIR/plugins/braze/src/main/res/drawable-xxxhdpi/ic_notification.png\nfi\n\n# moengage notification icon\nif [[ $1 == \"moengage\" ]]\nthen\n  cp $BASEDIR/app/src/main/res/drawable-mdpi/ic_notification.png $BASEDIR/plugins/moengage/src/main/res/drawable-mdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-hdpi/ic_notification.png $BASEDIR/plugins/moengage/src/main/res/drawable-hdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xhdpi/ic_notification.png $BASEDIR/plugins/moengage/src/main/res/drawable-xhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxhdpi/ic_notification.png $BASEDIR/plugins/moengage/src/main/res/drawable-xxhdpi/ic_notification.png\n  cp $BASEDIR/app/src/main/res/drawable-xxxhdpi/ic_notification.png $BASEDIR/plugins/moengage/src/main/res/drawable-xxxhdpi/ic_notification.png\nfi\n"
  },
  {
    "path": "generate-theme.js",
    "content": "#!/usr/bin/env node\n'use strict';\n\nvar fs = require('fs'), xml2js = require('xml2js');\nvar builder = new xml2js.Builder();\n\n// Color resources files\nvar colorFileLight = require('path').join(__dirname, 'app/src/main/res/values/colors.xml');\nvar colorFileDark = require('path').join(__dirname, 'app/src/main/res/values-night/colors.xml');\n\n// Check argument length\nif (process.argv.length != 20) {\n  console.log('Incomplete args - ' + process.argv.length + '/19');\n  showHelp();\n}\n\n// LIGHT theme color defaults\n\nvar LIGHT_DEFAULT_ACTIONBAR_COLOR = 'ffffff';\nvar LIGHT_DEFAULT_STATUSBAR_COLOR = '757575';\nvar LIGHT_DEFAULT_TITLE_COLOR = '000000';\nvar LIGHT_DEFAULT_ACCENT_COLOR = '009688';\nvar LIGHT_DEFAULT_BACKGROUND_COLOR = 'ffffff';\nvar LIGHT_DEFAULT_SIDEBAR_FOREGROUND_COLOR = '1e4963';\nvar LIGHT_DEFAULT_SIDEBAR_BACKGROUND_COLOR = 'ffffff';\nvar LIGHT_DEFAULT_SIDEBAR_SEPARATOR_COLOR = '30808080';\nvar LIGHT_DEFAULT_SIDEBAR_HIGHLIGHT_COLOR = '1e496e';\nvar LIGHT_DEFAULT_TAB_BAR_BACKGROUND_COLOR = 'ffffff';\nvar LIGHT_DEFAULT_TAB_BAR_TEXT_COLOR = '949494';\nvar LIGHT_DEFAULT_TAB_BAR_INDICATOR_COLOR = '1e496e';\nvar LIGHT_DEFAULT_SWIPE_NAV_BACKGROUND_COLOR = 'fafafa';\nvar LIGHT_DEFAULT_SWIPE_NAV_INACTIVE_COLOR = '888888';\nvar LIGHT_DEFAULT_SWIPE_NAV_ACTIVE_COLOR = '1e88e5';\nvar LIGHT_DEFAULT_PULL_TO_REFRESH_COLOR = '1e496e';\nvar LIGHT_DEFAULT_SPLASH_BACKGROUND_COLOR = \"1e496e\";\n\n// DARK theme color defaults\n\nvar DARK_DEFAULT_ACTIONBAR_COLOR = '212121';\nvar DARK_DEFAULT_STATUSBAR_COLOR = '030303';\nvar DARK_DEFAULT_TITLE_COLOR = 'ffffff';\nvar DARK_DEFAULT_ACCENT_COLOR = '80cbc4';\nvar DARK_DEFAULT_BACKGROUND_COLOR = '444444';\nvar DARK_DEFAULT_SIDEBAR_FOREGROUND_COLOR = 'ffffff';\nvar DARK_DEFAULT_SIDEBAR_BACKGROUND_COLOR = '333333';\nvar DARK_DEFAULT_SIDEBAR_SEPARATOR_COLOR = '30ffffff';\nvar DARK_DEFAULT_SIDEBAR_HIGHLIGHT_COLOR = 'ffffff';\nvar DARK_DEFAULT_TAB_BAR_BACKGROUND_COLOR = '333333';\nvar DARK_DEFAULT_TAB_BAR_TEXT_COLOR = 'ffffff';\nvar DARK_DEFAULT_TAB_BAR_INDICATOR_COLOR = '666666';\nvar DARK_DEFAULT_SWIPE_NAV_BACKGROUND_COLOR = '666666';\nvar DARK_DEFAULT_SWIPE_NAV_INACTIVE_COLOR = '888888';\nvar DARK_DEFAULT_SWIPE_NAV_ACTIVE_COLOR = 'ffffff';\nvar DARK_DEFAULT_PULL_TO_REFRESH_COLOR = 'ffffff';\nvar DARK_DEFAULT_SPLASH_BACKGROUND_COLOR = '333333';\n\n// Handle args\n// Theme\nvar theme = process.argv[2];\n\n// Actionbar args\nvar actionBarColor = process.argv[3].toLowerCase();\nvar statusBarColor = process.argv[4].toLowerCase();\nvar titleColor = process.argv[5].toLowerCase();\nvar accentColor = process.argv[6].toLowerCase();\nvar backgroundColor = process.argv[7].toLowerCase();\n\n// Sidebar args\nvar sideBarForegroundColor = process.argv[8].toLowerCase();\nvar sideBarBackgroundColor = process.argv[9].toLowerCase();\nvar sidebarSeparatorColor = process.argv[10].toLowerCase();\nvar sidebarHighlightColor = process.argv[11].toLowerCase();\n\n// TabBar args\nvar tabBarBackground = process.argv[12].toLowerCase();\nvar tabBarTextColor = process.argv[13].toLowerCase();\nvar tabBarIndicatorColor = process.argv[14].toLowerCase();\n\n// Swipe args\nvar swipeBackgroundColor = process.argv[15].toLowerCase();\nvar swipeInactiveColor = process.argv[16].toLowerCase();\nvar swipeActiveColor = process.argv[17].toLowerCase();\n\n// Pull to Refresh args\nvar pullToRefresh = process.argv[18].toLowerCase();\n\n// Splash args\nvar splashBackgroundColor = process.argv[19].toLowerCase();\n\nvar defaultActionBarColor;\nvar defaultStatusBarColor;\nvar defaultTitleColor;\nvar defaultAccentColor;\nvar defaultBackgroundColor;\nvar defaultSidebarForegroundColor;\nvar defaultSidebarBackgroundColor;\nvar defaultSidebarSeparatorColor;\nvar defaultSidebarHighlightColor;\nvar defaultTabBarBackgroundColor;\nvar defaultTabBarTextColor;\nvar defaultTabBarIndicatorColor;\nvar defaultSwipeBackgroundColor;\nvar defaultSwipeInactiveColor;\nvar defaultSwipeActiveColor;\nvar defaultPullToRefreshColor;\nvar defaultSplashBackgroundColor;\n\n// Theme and default color values\nvar writeDir;\nif (theme === 'light' || theme === 'auto') {\n  writeDir = colorFileLight;\n\n  defaultActionBarColor = LIGHT_DEFAULT_ACTIONBAR_COLOR;\n  defaultStatusBarColor= LIGHT_DEFAULT_STATUSBAR_COLOR;\n  defaultTitleColor = LIGHT_DEFAULT_TITLE_COLOR;\n  defaultAccentColor = LIGHT_DEFAULT_ACCENT_COLOR;\n  defaultBackgroundColor = LIGHT_DEFAULT_BACKGROUND_COLOR;\n  defaultSidebarForegroundColor = LIGHT_DEFAULT_SIDEBAR_FOREGROUND_COLOR;\n  defaultSidebarBackgroundColor = LIGHT_DEFAULT_SIDEBAR_BACKGROUND_COLOR;\n  defaultSidebarSeparatorColor = LIGHT_DEFAULT_SIDEBAR_SEPARATOR_COLOR;\n  defaultSidebarHighlightColor = LIGHT_DEFAULT_SIDEBAR_HIGHLIGHT_COLOR;\n  defaultTabBarBackgroundColor = LIGHT_DEFAULT_TAB_BAR_BACKGROUND_COLOR;\n  defaultTabBarTextColor = LIGHT_DEFAULT_TAB_BAR_TEXT_COLOR;\n  defaultTabBarIndicatorColor = LIGHT_DEFAULT_TAB_BAR_INDICATOR_COLOR;\n  defaultSwipeBackgroundColor = LIGHT_DEFAULT_SWIPE_NAV_BACKGROUND_COLOR;\n  defaultSwipeInactiveColor = LIGHT_DEFAULT_SWIPE_NAV_INACTIVE_COLOR;\n  defaultSwipeActiveColor = LIGHT_DEFAULT_SWIPE_NAV_ACTIVE_COLOR;\n  defaultPullToRefreshColor = LIGHT_DEFAULT_PULL_TO_REFRESH_COLOR;\n  defaultSplashBackgroundColor = LIGHT_DEFAULT_SPLASH_BACKGROUND_COLOR;\n\n} else if (theme === 'dark' || theme === 'light.darkactionbar') {\n  writeDir = colorFileDark;\n\n  defaultActionBarColor = DARK_DEFAULT_ACTIONBAR_COLOR;\n  defaultStatusBarColor= DARK_DEFAULT_STATUSBAR_COLOR;\n  defaultTitleColor = DARK_DEFAULT_TITLE_COLOR;\n  defaultAccentColor = DARK_DEFAULT_ACCENT_COLOR;\n  defaultBackgroundColor = DARK_DEFAULT_BACKGROUND_COLOR;\n  defaultSidebarForegroundColor = DARK_DEFAULT_SIDEBAR_FOREGROUND_COLOR;\n  defaultSidebarBackgroundColor = DARK_DEFAULT_SIDEBAR_BACKGROUND_COLOR;\n  defaultSidebarSeparatorColor = DARK_DEFAULT_SIDEBAR_SEPARATOR_COLOR;\n  defaultSidebarHighlightColor = DARK_DEFAULT_SIDEBAR_HIGHLIGHT_COLOR;\n  defaultTabBarBackgroundColor = DARK_DEFAULT_TAB_BAR_BACKGROUND_COLOR;\n  defaultTabBarTextColor = DARK_DEFAULT_TAB_BAR_TEXT_COLOR;\n  defaultTabBarIndicatorColor = DARK_DEFAULT_TAB_BAR_INDICATOR_COLOR;\n  defaultSwipeBackgroundColor = DARK_DEFAULT_SWIPE_NAV_BACKGROUND_COLOR;\n  defaultSwipeInactiveColor = DARK_DEFAULT_SWIPE_NAV_INACTIVE_COLOR;\n  defaultSwipeActiveColor = DARK_DEFAULT_SWIPE_NAV_ACTIVE_COLOR;\n  defaultPullToRefreshColor = DARK_DEFAULT_PULL_TO_REFRESH_COLOR;\n  defaultSplashBackgroundColor = DARK_DEFAULT_SPLASH_BACKGROUND_COLOR;\n\n} else {\n  console.log('Invalid theme ' + theme);\n  showHelp();\n}\n\n// Check if colors are valid\ncheckColorRegex(actionBarColor, 'Invalid actionbar background color');\ncheckColorRegex(statusBarColor, 'Invalid status bar color');\ncheckColorRegex(titleColor, 'Invalid actionbar title color');\ncheckColorRegex(accentColor, 'Invalid accent color');\ncheckColorRegex(backgroundColor, 'Invalid background color');\ncheckColorRegex(sideBarForegroundColor, 'Invalid sidebar foreground color');\ncheckColorRegex(sideBarBackgroundColor, 'Invalid sidebar background color');\ncheckColorRegex(sidebarSeparatorColor, 'Invalid sidebar separator color');\ncheckColorRegex(sidebarHighlightColor, 'Invalid sidebar highlight color');\ncheckColorRegex(tabBarBackground, 'Invalid tab bar background color');\ncheckColorRegex(tabBarTextColor, 'Invalid tab bar text color');\ncheckColorRegex(tabBarIndicatorColor, 'Invalid tab bar indicator color');\ncheckColorRegex(swipeBackgroundColor, 'Invalid swipe nav background color');\ncheckColorRegex(swipeInactiveColor, 'Invalid swipe nav inactive color');\ncheckColorRegex(swipeActiveColor, 'Invalid swipe nav active color');\ncheckColorRegex(pullToRefresh, 'Invalid pullToRefresh color');\ncheckColorRegex(splashBackgroundColor, 'Invalid splash background color');\n\n// Start generating colors\nvar colorArray = [];\n\n// Actionbar colors\ncolorArray.push(createColorJSON('colorPrimary', actionBarColor, defaultActionBarColor));\ncolorArray.push(createColorJSON('colorPrimaryDark', statusBarColor, defaultStatusBarColor));\ncolorArray.push(createColorJSON('titleTextColor', titleColor, defaultTitleColor));\ncolorArray.push(createColorJSON('colorAccent', accentColor, defaultAccentColor));\ncolorArray.push(createColorJSON('colorBackground', backgroundColor, defaultBackgroundColor));\ncolorArray.push(createColorJSON('drawerArrow', titleColor, defaultTitleColor));\n\n// Sidebar colors\ncolorArray.push(createColorJSON('sidebarForeground', sideBarForegroundColor, defaultSidebarForegroundColor));\ncolorArray.push(createColorJSON('sidebarBackground', sideBarBackgroundColor, defaultSidebarBackgroundColor));\ncolorArray.push(createColorJSON('sidebarSeparatorColor', sidebarSeparatorColor, defaultSidebarSeparatorColor));\ncolorArray.push(createColorJSON('sidebarHighlight', sidebarHighlightColor, defaultSidebarHighlightColor));\n\n// TabBar colors\ncolorArray.push(createColorJSON('tabBarBackground', tabBarBackground, defaultTabBarBackgroundColor));\ncolorArray.push(createColorJSON('tabBarTextColor', tabBarTextColor, defaultTabBarTextColor));\ncolorArray.push(createColorJSON('tabBarIndicator', tabBarIndicatorColor, defaultTabBarIndicatorColor));\n\n// Swipe colors\ncolorArray.push(createColorJSON('swipe_nav_background', swipeBackgroundColor, defaultSwipeBackgroundColor));\ncolorArray.push(createColorJSON('swipe_nav_inactive', swipeInactiveColor, defaultSwipeInactiveColor));\ncolorArray.push(createColorJSON('swipe_nav_active', swipeActiveColor, defaultSwipeActiveColor));\n\n// Pull to Refresh color\ncolorArray.push(createColorJSON('pull_to_refresh_color', pullToRefresh, defaultPullToRefreshColor));\n\n// Splash colors\ncolorArray.push(createColorJSON('splash_background', splashBackgroundColor, defaultSplashBackgroundColor));\n\n// Build colors in xml from JSON array of colors\nvar xmlFinal = builder.buildObject({resources: colorArray});\n\n// Write to file\nfs.writeFile(writeDir, xmlFinal, (err) => {\n    if (err)\n      console.log(err);\n    else {\n      console.log(\"File written successfully\\n\");\n    }\n});\n\n// Helper functions\n\nfunction showHelp() {\n  console.log('Usage: generate-theme.js (dark|light|light.darkactionbar|auto) ' +\n          'actionBarColor statusBarColor titleColor accentColor backgroundColor ' +\n          'sidebarForegroundColor sidebarBackgroundColor sidebarSeparatorColor sidebarHighlightColor ' +\n          'tabBarBackgroundColor tabBarTextColor tabBarIndicatorColor ' +\n          'swipeBackgroundColor swipeInactiveColor swipeActiveColor pullToRefreshColor splashBackgroundColor');\n  console.log('Example: generate-theme.js light dcdcdc 757575 000000 ff0000 ...');\n  console.log('Colors can be blank for default');\n  process.exit(1);\n}\n\nfunction checkColorRegex(color, message) {\n  if (color !== '' && !/^(([0-9a-f]){6}|([0-9a-f]){8})$/.test(color)) {\n      console.log(message + ' ' + color);\n      showHelp();\n  }\n}\n\n// Creates a JSON object of color which will be then transformed to xml\nfunction createColorJSON(colorName, colorCode, defaultColor) {\n  if (colorCode !== '') {\n    return {color: {$: {name: colorName}, _: '#' + colorCode}};\n  } else {\n    return {color: {$: {name: colorName}, _: '#' + defaultColor}};\n  }\n}\n"
  },
  {
    "path": "generate-tinted-icons.sh",
    "content": "#!/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\nLIGHT_ICONS=(\nic_refresh_black_24dp.png\nic_search_black_24dp.png\nic_share_black_24dp.png\n)\n\nDARK_DEFAULT_COLOR=ffffff\nLIGHT_DEFAULT_COLOR=000000\n\nBASEDIR=$(dirname $0)\n\nfunction showHelp {\n    echo \"Usage: $0 (dark|light) tintColor tintColorDark\"\n    echo \"Example: $0 light 0000ff ffffff\"\n    exit 1\n\n}\n\nif [[ $# -lt 2 ]]; then\n    showHelp\nfi\n\ntheme=`echo $1 | tr '[:upper:]' '[:lower:]'`\ntintColor=`echo $2 | tr '[:upper:]' '[:lower:]'`\ntintColorDark=`echo $3 | tr '[:upper:]' '[:lower:]'`\n\nif [[ $theme = light ]]; then\n    icons=(\"${LIGHT_ICONS[@]}\")\n    defaultColor=$LIGHT_DEFAULT_COLOR\nelif [[ $theme = dark ]]; then\n    icons=(\"${DARK_ICONS[@]}\")  \n    defaultColor=$DARK_DEFAULT_COLOR\nelse\n    showHelp\nfi\n\nif [[ ${#tintColor} -ne 6 ]]; then\n    showHelp\nfi\n\nif [[ $tintColor = $defaultColor ]]; then\n    echo \"Requested color $tintColor is same as default for theme. Exiting.\"\n    exit\nfi\n\nfor drawable in `ls -d $BASEDIR/app/src/main/res/drawable*`; do\n    for file in  \"${icons[@]}\"; do\n        filePath=$drawable/$file\n        if [[ -s \"$filePath\" ]]; then\n            echo Tinting $filePath\n            convert $filePath -fill \"#$tintColor\" -colorize 100% $filePath\n            optipng $filePath\n        fi\n    done\ndone\n\n\nif [[ ${#tintColorDark} -ne 6 ]]; then\n    exit\nfi\n\nfor drawable in `ls -d $BASEDIR/app/src/main/res/drawable-night*`; do\n    for file in  \"${icons[@]}\"; do\n        filePath=$drawable/$file\n        if [[ -s \"$filePath\" ]]; then\n            echo Tinting $filePath\n            convert $filePath -fill \"#$tintColorDark\" -colorize 100% $filePath\n            optipng $filePath\n        fi\n    done\ndone"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Sat Feb 18 14:21:02 EST 2017\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-bin.zip\n"
  },
  {
    "path": "gradle.properties",
    "content": "org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m\nandroid.enableJetifier=true\nandroid.useAndroidX=true\nenableLogsInRelease=false"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS=\"\"\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn ( ) {\n    echo \"$*\"\n}\n\ndie ( ) {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\nesac\n\n# For Cygwin, ensure paths are in UNIX format before anything is touched.\nif $cygwin ; then\n    [ -n \"$JAVA_HOME\" ] && JAVA_HOME=`cygpath --unix \"$JAVA_HOME\"`\nfi\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >&-\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >&-\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin, switch paths to Windows format before running java\nif $cygwin ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=$((i+1))\n    done\n    case $i in\n        (0) set -- ;;\n        (1) set -- \"$args0\" ;;\n        (2) set -- \"$args0\" \"$args1\" ;;\n        (3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        (4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        (5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        (6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        (7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        (8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        (9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules\nfunction splitJvmOpts() {\n    JVM_OPTS=(\"$@\")\n}\neval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\nJVM_OPTS[${#JVM_OPTS[*]}]=\"-Dorg.gradle.appname=$APP_BASE_NAME\"\n\nexec \"$JAVACMD\" \"${JVM_OPTS[@]}\" -classpath \"$CLASSPATH\" org.gradle.wrapper.GradleWrapperMain \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@if \"%DEBUG%\" == \"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif \"%ERRORLEVEL%\" == \"0\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto init\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:init\r\n@rem Get command-line arguments, handling Windowz variants\r\n\r\nif not \"%OS%\" == \"Windows_NT\" goto win9xME_args\r\nif \"%@eval[2+2]\" == \"4\" goto 4NT_args\r\n\r\n:win9xME_args\r\n@rem Slurp the command line arguments.\r\nset CMD_LINE_ARGS=\r\nset _SKIP=2\r\n\r\n:win9xME_args_slurp\r\nif \"x%~1\" == \"x\" goto execute\r\n\r\nset CMD_LINE_ARGS=%*\r\ngoto execute\r\n\r\n:4NT_args\r\n@rem Get arguments from the 4NT Shell from JP Software\r\nset CMD_LINE_ARGS=%$\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\r\nexit /b 1\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "plugins.gradle",
    "content": "import groovy.json.JsonSlurper\nimport org.gradle.initialization.DefaultSettings\nimport org.apache.tools.ant.taskdefs.condition.Os\n\ndef generatedFileName = \"PackageList.java\"\ndef generatedFilePackage = \"io.gonative.android\"\ndef generatedFileContentsTemplate = \"\"\"\npackage $generatedFilePackage;\n\nimport android.app.Application;\nimport android.content.Context;\nimport android.content.res.Resources;\n\nimport io.gonative.gonative_core.BridgeModule;\nimport java.util.Arrays;\nimport java.util.ArrayList;\n\n{{ packageImports }}\n\npublic class PackageList {\n  private Application application;\n\n  public PackageList(Application application) {\n    this.application = application;\n  }\n\n  private Resources getResources() {\n    return this.getApplication().getResources();\n  }\n\n  private Application getApplication() {\n    return this.application;\n  }\n\n  private Context getApplicationContext() {\n    return this.getApplication().getApplicationContext();\n  }\n\n  public ArrayList<BridgeModule> getPackages() {\n    return new ArrayList<>(Arrays.<BridgeModule>asList(\n      {{ packageClassInstances }}\n    ));\n  }\n}\n\"\"\"\n\nclass GoNativeModules {\n    private Logger logger\n    private ArrayList<HashMap<String, String>> modulesMetadata\n\n    private packageName = \"io.gonative.android\"\n\n    GoNativeModules(Logger logger) {\n        this.logger = logger\n        this.modulesMetadata = this.getModulesMetadata()\n    }\n\n    ArrayList<HashMap<String, String>> getModulesMetadata() {\n        if (this.modulesMetadata != null) return this.modulesMetadata\n\n        ArrayList<HashMap<String, String>> modulesMetadata = new ArrayList<HashMap<String, String>>()\n\n        def finder = new FileNameFinder()\n        def files = finder.getFileNames(System.getProperty(\"user.dir\"), 'plugins/**/plugin-metadata.json')\n        files.each { fileName ->\n            def jsonFile = new File(fileName)\n            def parsedJson = new JsonSlurper().parseText(jsonFile.text).plugin\n            parsedJson[\"sourceDir\"] = fileName.tokenize(File.separator)[-3..-2].join(File.separator)\n            modulesMetadata.push(parsedJson)\n        }\n\n        return modulesMetadata\n    }\n\n    void addModuleProjects(DefaultSettings defaultSettings) {\n        modulesMetadata.forEach { module ->\n            String pluginName = module[\"pluginName\"]\n            String sourceDir = module[\"sourceDir\"]\n            this.logger.warn(sourceDir)\n            defaultSettings.include(\":${pluginName}\")\n            defaultSettings.project(\":${pluginName}\").projectDir = new File(defaultSettings.rootProject.projectDir, \"./${sourceDir}\")\n        }\n    }\n\n    void addModuleDependencies(Project appProject) {\n        modulesMetadata.forEach { module ->\n            String pluginName = module[\"pluginName\"]\n            appProject.dependencies {\n                implementation project(path: \":${pluginName}\")\n            }\n        }\n    }\n\n    void generatePackagesFile(File outputDir, String generatedFileName, GString generatedFileContentsTemplate) {\n        def packages = this.modulesMetadata\n        String packageName = this.packageName\n\n        String packageImports = \"\"\n        String packageClassInstances = \"\"\n\n        if (packages.size() > 0) {\n            packageImports = \"import ${packageName}.BuildConfig;\\nimport ${packageName}.R;\\n\\n\"\n            packageImports = packageImports + packages.collect {\n                \"// ${it.name}\\nimport ${it.packageName}.${it.classInstance};\"\n            }.join('\\n')\n            packageClassInstances = packages.collect { \"new ${it.classInstance}()\" }.join(\",\\n      \")\n        }\n\n        String generatedFileContents = generatedFileContentsTemplate.toString()\n                .replace(\"{{ packageImports }}\", packageImports)\n                .replace(\"{{ packageClassInstances }}\", packageClassInstances)\n\n        outputDir.mkdirs()\n        final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)\n        treeBuilder.file(generatedFileName).newWriter().withWriter { w ->\n            w << generatedFileContents\n        }\n    }\n}\n\ndef gonativeModules = new GoNativeModules(logger)\n\next.applyModulesSettingsGradle = { DefaultSettings defaultSettings ->\n    gonativeModules.addModuleProjects(defaultSettings)\n}\n\next.applyNativeModulesAppBuildGradle = { Project project ->\n    gonativeModules.addModuleDependencies(project)\n\n    def generatedSrcDir = new File(buildDir, \"generated/gncli/src/main/java\")\n    def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))\n\n    task generatePackageList {\n        doLast {\n            gonativeModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)\n        }\n    }\n\n    preBuild.dependsOn generatePackageList\n\n    android {\n        sourceSets {\n            main {\n                java {\n                    srcDirs += generatedSrcDir\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'GoNative'\nSystem.setProperty(\"user.dir\", rootProject.projectDir.toString())\napply from: file(\"./plugins.gradle\"); applyModulesSettingsGradle(settings)\n\ninclude ':app'\n"
  }
]