Initial Commit on git.saherelm.ir ...

This commit is contained in:
2026-08-20 15:42:40 +03:30
commit fac270d301
779 changed files with 79360 additions and 0 deletions
+415
View File
@@ -0,0 +1,415 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
apply plugin: 'com.android.application'
if (cdvHelpers.getConfigPreference('GradlePluginKotlinEnabled', 'false').toBoolean()) {
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
}
buildscript {
apply from: '../CordovaLib/cordova.gradle'
if(cdvHelpers.getConfigPreference('GradlePluginKotlinEnabled', 'false').toBoolean()) {
String defaultGradlePluginKotlinVersion = kotlin_version
/**
* Fetches the user's defined Kotlin Version from config.xml.
* If the version is not set or invalid, it will default to the ${defaultGradlePluginKotlinVersion}
*/
String gradlePluginKotlinVersion = cdvHelpers.getConfigPreference('GradlePluginKotlinVersion', defaultGradlePluginKotlinVersion)
if(!cdvHelpers.isVersionValid(gradlePluginKotlinVersion)) {
println("The defined Kotlin version (${gradlePluginKotlinVersion}) does not appear to be a valid version. Falling back to version: ${defaultGradlePluginKotlinVersion}.")
gradlePluginKotlinVersion = defaultGradlePluginKotlinVersion
}
// Change the version to be used.
ext.kotlin_version = gradlePluginKotlinVersion
}
apply from: 'repositories.gradle'
repositories repos
dependencies {
apply from: '../CordovaLib/cordova.gradle'
classpath 'com.android.tools.build:gradle:4.0.0'
if (cdvHelpers.getConfigPreference('GradlePluginKotlinEnabled', 'false').toBoolean()) {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
if(cdvHelpers.getConfigPreference('GradlePluginGoogleServicesEnabled', 'false').toBoolean()) {
String defaultGradlePluginGoogleServicesVersion = '4.2.0'
/**
* Fetches the user's defined Google Services Plugin Version from config.xml.
* If the version is not set or invalid, it will default to the ${defaultGradlePluginGoogleServicesVersion}
*/
String gradlePluginGoogleServicesVersion = cdvHelpers.getConfigPreference('GradlePluginGoogleServicesVersion', defaultGradlePluginGoogleServicesVersion)
if(!cdvHelpers.isVersionValid(gradlePluginGoogleServicesVersion)) {
println("The defined Google Services plugin version (${gradlePluginGoogleServicesVersion}) does not appear to be a valid version. Falling back to version: ${defaultGradlePluginGoogleServicesVersion}.")
gradlePluginGoogleServicesVersion = defaultGradlePluginGoogleServicesVersion
}
// Create the Google Services classpath and set it.
String gradlePluginGoogleServicesClassPath = "com.google.gms:google-services:${gradlePluginGoogleServicesVersion}"
println "Adding classpath: ${gradlePluginGoogleServicesClassPath}"
classpath gradlePluginGoogleServicesClassPath
}
}
}
// Allow plugins to declare Maven dependencies via build-extras.gradle.
allprojects {
apply from: 'repositories.gradle'
repositories repos
}
task wrapper(type: Wrapper) {
gradleVersion = '6.5'
}
// Configuration properties. Set these via environment variables, build-extras.gradle, or gradle.properties.
// Refer to: http://www.gradle.org/docs/current/userguide/tutorial_this_and_that.html
ext {
apply from: '../CordovaLib/cordova.gradle'
// The value for android.compileSdkVersion.
if (!project.hasProperty('cdvCompileSdkVersion')) {
cdvCompileSdkVersion = null;
}
// The value for android.buildToolsVersion.
if (!project.hasProperty('cdvBuildToolsVersion')) {
cdvBuildToolsVersion = null;
}
// Sets the versionCode to the given value.
if (!project.hasProperty('cdvVersionCode')) {
cdvVersionCode = null
}
// Sets the minSdkVersion to the given value.
if (!project.hasProperty('cdvMinSdkVersion')) {
cdvMinSdkVersion = null
}
// Sets the maxSdkVersion to the given value.
if (!project.hasProperty('cdvMaxSdkVersion')) {
cdvMaxSdkVersion = null
}
// The value for android.targetSdkVersion.
if (!project.hasProperty('cdvTargetSdkVersion')) {
cdvTargetSdkVersion = null;
}
// Whether to build architecture-specific APKs.
if (!project.hasProperty('cdvBuildMultipleApks')) {
cdvBuildMultipleApks = null
}
// Whether to append a 0 "abi digit" to versionCode when only a single APK is build
if (!project.hasProperty('cdvVersionCodeForceAbiDigit')) {
cdvVersionCodeForceAbiDigit = null
}
// .properties files to use for release signing.
if (!project.hasProperty('cdvReleaseSigningPropertiesFile')) {
cdvReleaseSigningPropertiesFile = null
}
// .properties files to use for debug signing.
if (!project.hasProperty('cdvDebugSigningPropertiesFile')) {
cdvDebugSigningPropertiesFile = null
}
// Set by build.js script.
if (!project.hasProperty('cdvBuildArch')) {
cdvBuildArch = null
}
// Plugin gradle extensions can append to this to have code run at the end.
cdvPluginPostBuildExtras = []
}
// PLUGIN GRADLE EXTENSIONS START
// PLUGIN GRADLE EXTENSIONS END
def hasBuildExtras1 = file('build-extras.gradle').exists()
if (hasBuildExtras1) {
apply from: 'build-extras.gradle'
}
def hasBuildExtras2 = file('../build-extras.gradle').exists()
if (hasBuildExtras2) {
apply from: '../build-extras.gradle'
}
// Set property defaults after extension .gradle files.
ext.cdvCompileSdkVersion = cdvCompileSdkVersion == null ? (
defaultCompileSdkVersion == null
? privateHelpers.getProjectTarget()
: defaultCompileSdkVersion
) : Integer.parseInt('' + cdvCompileSdkVersion);
if (ext.cdvBuildToolsVersion == null) {
ext.cdvBuildToolsVersion = privateHelpers.findLatestInstalledBuildTools()
//ext.cdvBuildToolsVersion = project.ext.defaultBuildToolsVersion
}
if (ext.cdvDebugSigningPropertiesFile == null && file('../debug-signing.properties').exists()) {
ext.cdvDebugSigningPropertiesFile = '../debug-signing.properties'
}
if (ext.cdvReleaseSigningPropertiesFile == null && file('../release-signing.properties').exists()) {
ext.cdvReleaseSigningPropertiesFile = '../release-signing.properties'
}
// Cast to appropriate types.
ext.cdvBuildMultipleApks = cdvBuildMultipleApks == null ? false : cdvBuildMultipleApks.toBoolean();
ext.cdvVersionCodeForceAbiDigit = cdvVersionCodeForceAbiDigit == null ? false : cdvVersionCodeForceAbiDigit.toBoolean();
// minSdkVersion, maxSdkVersion and targetSdkVersion
ext.cdvMinSdkVersion = cdvMinSdkVersion == null ? defaultMinSdkVersion : Integer.parseInt('' + cdvMinSdkVersion)
if (cdvMaxSdkVersion != null) {
ext.cdvMaxSdkVersion = Integer.parseInt('' + cdvMaxSdkVersion)
}
ext.cdvTargetSdkVersion = cdvTargetSdkVersion == null ? defaultTargetSdkVersion : Integer.parseInt('' + cdvTargetSdkVersion)
ext.cdvVersionCode = cdvVersionCode == null ? null : Integer.parseInt('' + cdvVersionCode)
def computeBuildTargetName(debugBuild) {
def ret = 'assemble'
if (cdvBuildMultipleApks && cdvBuildArch) {
def arch = cdvBuildArch == 'arm' ? 'armv7' : cdvBuildArch
ret += '' + arch.toUpperCase().charAt(0) + arch.substring(1);
}
return ret + (debugBuild ? 'Debug' : 'Release')
}
// Make cdvBuild a task that depends on the debug/arch-sepecific task.
task cdvBuildDebug
cdvBuildDebug.dependsOn {
return computeBuildTargetName(true)
}
task cdvBuildRelease
cdvBuildRelease.dependsOn {
return computeBuildTargetName(false)
}
task cdvPrintProps {
doLast {
println('cdvCompileSdkVersion=' + cdvCompileSdkVersion)
println('cdvBuildToolsVersion=' + cdvBuildToolsVersion)
println('cdvVersionCode=' + cdvVersionCode)
println('cdvVersionCodeForceAbiDigit=' + cdvVersionCodeForceAbiDigit)
println('cdvMinSdkVersion=' + cdvMinSdkVersion)
println('cdvMaxSdkVersion=' + cdvMaxSdkVersion)
println('cdvTargetSdkVersion=' + cdvTargetSdkVersion)
println('cdvBuildMultipleApks=' + cdvBuildMultipleApks)
println('cdvReleaseSigningPropertiesFile=' + cdvReleaseSigningPropertiesFile)
println('cdvDebugSigningPropertiesFile=' + cdvDebugSigningPropertiesFile)
println('cdvBuildArch=' + cdvBuildArch)
println('computedVersionCode=' + android.defaultConfig.versionCode)
android.productFlavors.each { flavor ->
println('computed' + flavor.name.capitalize() + 'VersionCode=' + flavor.versionCode)
}
}
}
android {
defaultConfig {
versionCode cdvVersionCode ?: new BigInteger("" + privateHelpers.extractIntFromManifest("versionCode"))
applicationId privateHelpers.extractStringFromManifest("package")
if (cdvMinSdkVersion != null) {
minSdkVersion cdvMinSdkVersion
}
if (cdvMaxSdkVersion != null) {
maxSdkVersion cdvMaxSdkVersion
}
if(cdvTargetSdkVersion != null) {
targetSdkVersion cdvTargetSdkVersion
}
}
lintOptions {
abortOnError false;
}
compileSdkVersion cdvCompileSdkVersion
buildToolsVersion cdvBuildToolsVersion
// This code exists for Crosswalk and other Native APIs.
// By default, we multiply the existing version code in the
// Android Manifest by 10 and add a number for each architecture.
// If you are not using Crosswalk or SQLite, you can
// ignore this chunk of code, and your version codes will be respected.
if (Boolean.valueOf(cdvBuildMultipleApks)) {
flavorDimensions "default"
productFlavors {
armeabi {
versionCode defaultConfig.versionCode*10 + 1
ndk {
abiFilters = ["armeabi"]
}
}
armv7 {
versionCode defaultConfig.versionCode*10 + 2
ndk {
abiFilters = ["armeabi-v7a"]
}
}
arm64 {
versionCode defaultConfig.versionCode*10 + 3
ndk {
abiFilters = ["arm64-v8a"]
}
}
x86 {
versionCode defaultConfig.versionCode*10 + 4
ndk {
abiFilters = ["x86"]
}
}
x86_64 {
versionCode defaultConfig.versionCode*10 + 5
ndk {
abiFilters = ["x86_64"]
}
}
}
} else if (Boolean.valueOf(cdvVersionCodeForceAbiDigit)) {
// This provides compatibility to the default logic for versionCode before cordova-android 5.2.0
defaultConfig {
versionCode defaultConfig.versionCode*10
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
if (cdvReleaseSigningPropertiesFile) {
signingConfigs {
release {
// These must be set or Gradle will complain (even if they are overridden).
keyAlias = ""
keyPassword = "__unset" // And these must be set to non-empty in order to have the signing step added to the task graph.
storeFile = null
storePassword = "__unset"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
addSigningProps(cdvReleaseSigningPropertiesFile, signingConfigs.release)
}
if (cdvDebugSigningPropertiesFile) {
addSigningProps(cdvDebugSigningPropertiesFile, signingConfigs.debug)
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
/*
* WARNING: Cordova Lib and platform scripts do management inside of this code here,
* if you are adding the dependencies manually, do so outside the comments, otherwise
* the Cordova tools will overwrite them
*/
dependencies {
implementation fileTree(dir: 'libs', include: '*.jar')
if (cdvHelpers.getConfigPreference('GradlePluginKotlinEnabled', 'false').toBoolean()) {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
// SUB-PROJECT DEPENDENCIES START
implementation(project(path: ":CordovaLib"))
implementation "com.android.support:support-annotations:27.+"
// SUB-PROJECT DEPENDENCIES END
}
def promptForReleaseKeyPassword() {
if (!cdvReleaseSigningPropertiesFile) {
return;
}
if ('__unset'.equals(android.signingConfigs.release.storePassword)) {
android.signingConfigs.release.storePassword = privateHelpers.promptForPassword('Enter key store password: ')
}
if ('__unset'.equals(android.signingConfigs.release.keyPassword)) {
android.signingConfigs.release.keyPassword = privateHelpers.promptForPassword('Enter key password: ');
}
}
gradle.taskGraph.whenReady { taskGraph ->
taskGraph.getAllTasks().each() { task ->
if(['validateReleaseSigning', 'validateSigningRelease', 'validateSigningArmv7Release', 'validateSigningX76Release'].contains(task.name)) {
promptForReleaseKeyPassword()
}
}
}
def addSigningProps(propsFilePath, signingConfig) {
def propsFile = file(propsFilePath)
def props = new Properties()
propsFile.withReader { reader ->
props.load(reader)
}
def storeFile = new File(props.get('key.store') ?: privateHelpers.ensureValueExists(propsFilePath, props, 'storeFile'))
if (!storeFile.isAbsolute()) {
storeFile = RelativePath.parse(true, storeFile.toString()).getFile(propsFile.getParentFile())
}
if (!storeFile.exists()) {
throw new FileNotFoundException('Keystore file does not exist: ' + storeFile.getAbsolutePath())
}
signingConfig.keyAlias = props.get('key.alias') ?: privateHelpers.ensureValueExists(propsFilePath, props, 'keyAlias')
signingConfig.keyPassword = props.get('keyPassword', props.get('key.alias.password', signingConfig.keyPassword))
signingConfig.storeFile = storeFile
signingConfig.storePassword = props.get('storePassword', props.get('key.store.password', signingConfig.storePassword))
def storeType = props.get('storeType', props.get('key.store.type', ''))
if (!storeType) {
def filename = storeFile.getName().toLowerCase();
if (filename.endsWith('.p12') || filename.endsWith('.pfx')) {
storeType = 'pkcs12'
} else {
storeType = signingConfig.storeType // "jks"
}
}
signingConfig.storeType = storeType
}
for (def func : cdvPluginPostBuildExtras) {
func()
}
// This can be defined within build-extras.gradle as:
// ext.postBuildExtras = { ... code here ... }
if (hasProperty('postBuildExtras')) {
postBuildExtras()
}
if (cdvHelpers.getConfigPreference('GradlePluginGoogleServicesEnabled', 'false').toBoolean()) {
apply plugin: 'com.google.gms.google-services'
}
+23
View File
@@ -0,0 +1,23 @@
/* Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
ext.repos = {
mavenCentral()
google()
jcenter()
}
@@ -0,0 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<manifest android:hardwareAccelerated="true" android:versionCode="10000" android:versionName="1.0.0" package="ir.xFramework.xFrameworkTest" xmlns:android="http://schemas.android.com/apk/res/android">
<supports-screens android:anyDensity="true" android:largeScreens="true" android:normalScreens="true" android:resizeable="true" android:smallScreens="true" android:xlargeScreens="true" />
<uses-permission android:name="android.permission.INTERNET" />
<application android:hardwareAccelerated="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" android:supportsRtl="true">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode" android:label="@string/activity_name" android:launchMode="singleTop" android:name="MainActivity" android:theme="@android:style/Theme.DeviceDefault.NoActionBar" android:windowSoftInputMode="adjustResize">
<intent-filter android:label="@string/launcher_name">
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -0,0 +1,107 @@
package com.ionicframework.cordova.webview;
// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.util.Log;
import android.util.TypedValue;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
public class AndroidProtocolHandler {
private static final String TAG = "AndroidProtocolHandler";
private Context context;
public AndroidProtocolHandler(Context context) {
this.context = context;
}
public InputStream openAsset(String path) throws IOException {
return context.getAssets().open(path, AssetManager.ACCESS_STREAMING);
}
public InputStream openResource(Uri uri) {
assert uri.getPath() != null;
// The path must be of the form ".../asset_type/asset_name.ext".
List<String> pathSegments = uri.getPathSegments();
String assetType = pathSegments.get(pathSegments.size() - 2);
String assetName = pathSegments.get(pathSegments.size() - 1);
// Drop the file extension.
assetName = assetName.split("\\.")[0];
try {
// Use the application context for resolving the resource package name so that we do
// not use the browser's own resources. Note that if 'context' here belongs to the
// test suite, it does not have a separate application context. In that case we use
// the original context object directly.
if (context.getApplicationContext() != null) {
context = context.getApplicationContext();
}
int fieldId = getFieldId(context, assetType, assetName);
int valueType = getValueType(context, fieldId);
if (valueType == TypedValue.TYPE_STRING) {
return context.getResources().openRawResource(fieldId);
} else {
Log.e(TAG, "Asset not of type string: " + uri);
return null;
}
} catch (ClassNotFoundException e) {
Log.e(TAG, "Unable to open resource URL: " + uri, e);
return null;
} catch (NoSuchFieldException e) {
Log.e(TAG, "Unable to open resource URL: " + uri, e);
return null;
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to open resource URL: " + uri, e);
return null;
}
}
public InputStream openFile(String filePath) throws IOException {
String realPath = filePath.replace(WebViewLocalServer.fileStart, "");
File localFile = new File(realPath);
return new FileInputStream(localFile);
}
public InputStream openContentUrl(Uri uri) throws IOException {
Integer port = uri.getPort();
String realPath;
if (port == -1) {
realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + WebViewLocalServer.contentStart, "content:/");
} else {
realPath = uri.toString().replace(uri.getScheme() + "://" + uri.getHost() + ":" + port + WebViewLocalServer.contentStart, "content:/");
}
InputStream stream = null;
try {
stream = context.getContentResolver().openInputStream(Uri.parse(realPath));
} catch (SecurityException e) {
Log.e(TAG, "Unable to open content URL: " + uri, e);
}
return stream;
}
private static int getFieldId(Context context, String assetType, String assetName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
Class<?> d = context.getClassLoader()
.loadClass(context.getPackageName() + ".R$" + assetType);
java.lang.reflect.Field field = d.getField(assetName);
int id = field.getInt(null);
return id;
}
private static int getValueType(Context context, int fieldId) {
TypedValue value = new TypedValue();
context.getResources().getValue(fieldId, value, true);
return value.type;
}
}
@@ -0,0 +1,41 @@
package com.ionicframework.cordova.webview;
import android.app.Activity;
import android.content.SharedPreferences;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
public class IonicWebView extends CordovaPlugin {
public static final String WEBVIEW_PREFS_NAME = "WebViewSettings";
public static final String CDV_SERVER_PATH = "serverBasePath";
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("setServerBasePath")) {
final String path = args.getString(0);
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
((IonicWebViewEngine)webView.getEngine()).setServerBasePath(path);
}
});
return true;
} else if (action.equals("getServerBasePath")) {
callbackContext.success(((IonicWebViewEngine)webView.getEngine()).getServerBasePath());
return true;
} else if (action.equals("persistServerBasePath")) {
String path = ((IonicWebViewEngine)webView.getEngine()).getServerBasePath();
SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString(CDV_SERVER_PATH, path);
editor.apply();
return true;
}
return false;
}
}
@@ -0,0 +1,181 @@
package com.ionicframework.cordova.webview;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.annotation.TargetApi;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.util.Log;
import android.webkit.ServiceWorkerController;
import android.webkit.ServiceWorkerClient;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebSettings;
import android.webkit.WebView;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPreferences;
import org.apache.cordova.CordovaResourceApi;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaWebViewEngine;
import org.apache.cordova.NativeToJsMessageQueue;
import org.apache.cordova.PluginManager;
import org.apache.cordova.engine.SystemWebViewClient;
import org.apache.cordova.engine.SystemWebViewEngine;
import org.apache.cordova.engine.SystemWebView;
public class IonicWebViewEngine extends SystemWebViewEngine {
public static final String TAG = "IonicWebViewEngine";
private WebViewLocalServer localServer;
private String CDV_LOCAL_SERVER;
private String scheme;
private static final String LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode";
private static final String LAST_BINARY_VERSION_NAME = "lastBinaryVersionName";
/**
* Used when created via reflection.
*/
public IonicWebViewEngine(Context context, CordovaPreferences preferences) {
super(new SystemWebView(context), preferences);
Log.d(TAG, "Ionic Web View Engine Starting Right Up 1...");
}
public IonicWebViewEngine(SystemWebView webView) {
super(webView, null);
Log.d(TAG, "Ionic Web View Engine Starting Right Up 2...");
}
public IonicWebViewEngine(SystemWebView webView, CordovaPreferences preferences) {
super(webView, preferences);
Log.d(TAG, "Ionic Web View Engine Starting Right Up 3...");
}
@Override
public void init(CordovaWebView parentWebView, CordovaInterface cordova, final CordovaWebViewEngine.Client client,
CordovaResourceApi resourceApi, PluginManager pluginManager,
NativeToJsMessageQueue nativeToJsMessageQueue) {
ConfigXmlParser parser = new ConfigXmlParser();
parser.parse(cordova.getActivity());
String hostname = preferences.getString("Hostname", "localhost");
scheme = preferences.getString("Scheme", "http");
CDV_LOCAL_SERVER = scheme + "://" + hostname;
localServer = new WebViewLocalServer(cordova.getActivity(), hostname, true, parser, scheme);
localServer.hostAssets("www");
webView.setWebViewClient(new ServerClient(this, parser));
super.init(parentWebView, cordova, client, resourceApi, pluginManager, nativeToJsMessageQueue);
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
final WebSettings settings = webView.getSettings();
int mode = preferences.getInteger("MixedContentMode", 0);
settings.setMixedContentMode(mode);
}
SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
String path = prefs.getString(IonicWebView.CDV_SERVER_PATH, null);
if (!isDeployDisabled() && !isNewBinary() && path != null && !path.isEmpty()) {
setServerBasePath(path);
}
boolean setAsServiceWorkerClient = preferences.getBoolean("ResolveServiceWorkerRequests", false);
ServiceWorkerController controller = null;
if (setAsServiceWorkerClient && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
controller = ServiceWorkerController.getInstance();
controller.setServiceWorkerClient(new ServiceWorkerClient(){
@Override
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
return localServer.shouldInterceptRequest(request.getUrl(), request);
}
});
}
}
private boolean isNewBinary() {
String versionCode = "";
String versionName = "";
SharedPreferences prefs = cordova.getActivity().getApplicationContext().getSharedPreferences(IonicWebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
String lastVersionCode = prefs.getString(LAST_BINARY_VERSION_CODE, null);
String lastVersionName = prefs.getString(LAST_BINARY_VERSION_NAME, null);
try {
PackageInfo pInfo = this.cordova.getActivity().getPackageManager().getPackageInfo(this.cordova.getActivity().getPackageName(), 0);
versionCode = Integer.toString(pInfo.versionCode);
versionName = pInfo.versionName;
} catch(Exception ex) {
Log.e(TAG, "Unable to get package info", ex);
}
if (!versionCode.equals(lastVersionCode) || !versionName.equals(lastVersionName)) {
SharedPreferences.Editor editor = prefs.edit();
editor.putString(LAST_BINARY_VERSION_CODE, versionCode);
editor.putString(LAST_BINARY_VERSION_NAME, versionName);
editor.putString(IonicWebView.CDV_SERVER_PATH, "");
editor.apply();
return true;
}
return false;
}
private boolean isDeployDisabled() {
return preferences.getBoolean("DisableDeploy", false);
}
private class ServerClient extends SystemWebViewClient {
private ConfigXmlParser parser;
public ServerClient(SystemWebViewEngine parentEngine, ConfigXmlParser parser) {
super(parentEngine);
this.parser = parser;
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return localServer.shouldInterceptRequest(request.getUrl(), request);
}
@TargetApi(Build.VERSION_CODES.KITKAT)
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return localServer.shouldInterceptRequest(Uri.parse(url), null);
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
super.onPageStarted(view, url, favicon);
String launchUrl = parser.getLaunchUrl();
if (!launchUrl.contains(WebViewLocalServer.httpsScheme) && !launchUrl.contains(WebViewLocalServer.httpScheme) && url.equals(launchUrl)) {
view.stopLoading();
// When using a custom scheme the app won't load if server start url doesn't end in /
String startUrl = CDV_LOCAL_SERVER;
if (!scheme.equalsIgnoreCase(WebViewLocalServer.httpsScheme) && !scheme.equalsIgnoreCase(WebViewLocalServer.httpScheme)) {
startUrl += "/";
}
view.loadUrl(startUrl);
}
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
view.loadUrl("javascript:(function() { " +
"window.WEBVIEW_SERVER_URL = '" + CDV_LOCAL_SERVER + "';" +
"})()");
}
}
public void setServerBasePath(String path) {
localServer.hostFiles(path);
webView.loadUrl(CDV_LOCAL_SERVER);
}
public String getServerBasePath() {
return this.localServer.getBasePath();
}
}
@@ -0,0 +1,183 @@
package com.ionicframework.cordova.webview;
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//package com.google.webviewlocalserver.third_party.android;
import android.net.Uri;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public class UriMatcher {
/**
* Creates the root node of the URI tree.
*
* @param code the code to match for the root URI
*/
public UriMatcher(Object code) {
mCode = code;
mWhich = -1;
mChildren = new ArrayList<UriMatcher>();
mText = null;
}
private UriMatcher() {
mCode = null;
mWhich = -1;
mChildren = new ArrayList<UriMatcher>();
mText = null;
}
/**
* Add a URI to match, and the code to return when this URI is
* matched. URI nodes may be exact match string, the token "*"
* that matches any text, or the token "#" that matches only
* numbers.
* <p>
* Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
* this method will accept a leading slash in the path.
*
* @param authority the authority to match
* @param path the path to match. * may be used as a wild card for
* any text, and # may be used as a wild card for numbers.
* @param code the code that is returned when a URI is matched
* against the given components. Must be positive.
*/
public void addURI(String scheme, String authority, String path, Object code) {
if (code == null) {
throw new IllegalArgumentException("Code can't be null");
}
String[] tokens = null;
if (path != null) {
String newPath = path;
// Strip leading slash if present.
if (path.length() > 0 && path.charAt(0) == '/') {
newPath = path.substring(1);
}
tokens = PATH_SPLIT_PATTERN.split(newPath);
}
int numTokens = tokens != null ? tokens.length : 0;
UriMatcher node = this;
for (int i = -2; i < numTokens; i++) {
String token;
if (i == -2)
token = scheme;
else if (i == -1)
token = authority;
else
token = tokens[i];
ArrayList<UriMatcher> children = node.mChildren;
int numChildren = children.size();
UriMatcher child;
int j;
for (j = 0; j < numChildren; j++) {
child = children.get(j);
if (token.equals(child.mText)) {
node = child;
break;
}
}
if (j == numChildren) {
// Child not found, create it
child = new UriMatcher();
if (token.equals("**")) {
child.mWhich = REST;
} else if (token.equals("*")) {
child.mWhich = TEXT;
} else {
child.mWhich = EXACT;
}
child.mText = token;
node.mChildren.add(child);
node = child;
}
}
node.mCode = code;
}
static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/");
/**
* Try to match against the path in a url.
*
* @param uri The url whose path we will match against.
* @return The code for the matched node (added using addURI),
* or null if there is no matched node.
*/
public Object match(Uri uri) {
final List<String> pathSegments = uri.getPathSegments();
final int li = pathSegments.size();
UriMatcher node = this;
if (li == 0 && uri.getAuthority() == null) {
return this.mCode;
}
for (int i = -2; i < li; i++) {
String u;
if (i == -2)
u = uri.getScheme();
else if (i == -1)
u = uri.getAuthority();
else
u = pathSegments.get(i);
ArrayList<UriMatcher> list = node.mChildren;
if (list == null) {
break;
}
node = null;
int lj = list.size();
for (int j = 0; j < lj; j++) {
UriMatcher n = list.get(j);
which_switch:
switch (n.mWhich) {
case EXACT:
if (n.mText.equals(u)) {
node = n;
}
break;
case TEXT:
node = n;
break;
case REST:
return n.mCode;
}
if (node != null) {
break;
}
}
if (node == null) {
return null;
}
}
return node.mCode;
}
private static final int EXACT = 0;
private static final int TEXT = 1;
private static final int REST = 2;
private Object mCode;
private int mWhich;
private String mText;
private ArrayList<UriMatcher> mChildren;
}
@@ -0,0 +1,645 @@
/*
Copyright 2015 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package com.ionicframework.cordova.webview;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import org.apache.cordova.ConfigXmlParser;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Helper class meant to be used with the android.webkit.WebView class to enable hosting assets,
* resources and other data on 'virtual' http(s):// URL.
* Hosting assets and resources on http(s):// URLs is desirable as it is compatible with the
* Same-Origin policy.
* <p>
* This class is intended to be used from within the
* {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and
* {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
* android.webkit.WebResourceRequest)}
* methods.
*/
public class WebViewLocalServer {
private static String TAG = "WebViewAssetServer";
private String basePath;
public final static String httpScheme = "http";
public final static String httpsScheme = "https";
public final static String fileStart = "/_app_file_";
public final static String contentStart = "/_app_content_";
private final UriMatcher uriMatcher;
private final AndroidProtocolHandler protocolHandler;
private final String authority;
private final String customScheme;
// Whether we're serving local files or proxying (for example, when doing livereload on a
// non-local endpoint (will be false in that case)
private boolean isAsset;
// Whether to route all requests to paths without extensions back to `index.html`
private final boolean html5mode;
private ConfigXmlParser parser;
public String getAuthority() { return authority; }
/**
* A handler that produces responses for paths on the virtual asset server.
* <p>
* Methods of this handler will be invoked on a background thread and care must be taken to
* correctly synchronize access to any shared state.
* <p>
* On Android KitKat and above these methods may be called on more than one thread. This thread
* may be different than the thread on which the shouldInterceptRequest method was invoke.
* This means that on Android KitKat and above it is possible to block in this method without
* blocking other resources from loading. The number of threads used to parallelize loading
* is an internal implementation detail of the WebView and may change between updates which
* means that the amount of time spend blocking in this method should be kept to an absolute
* minimum.
*/
public abstract static class PathHandler {
protected String mimeType;
private String encoding;
private String charset;
private int statusCode;
private String reasonPhrase;
private Map<String, String> responseHeaders;
public PathHandler() {
this(null, null, 200, "OK", null);
}
public PathHandler(String encoding, String charset, int statusCode,
String reasonPhrase, Map<String, String> responseHeaders) {
this.encoding = encoding;
this.charset = charset;
this.statusCode = statusCode;
this.reasonPhrase = reasonPhrase;
Map<String, String> tempResponseHeaders;
if (responseHeaders == null) {
tempResponseHeaders = new HashMap<String, String>();
} else {
tempResponseHeaders = responseHeaders;
}
tempResponseHeaders.put("Cache-Control", "no-cache");
this.responseHeaders = tempResponseHeaders;
}
abstract public InputStream handle(Uri url);
public String getEncoding() {
return encoding;
}
public String getCharset() {
return charset;
}
public int getStatusCode() {
return statusCode;
}
public String getReasonPhrase() {
return reasonPhrase;
}
public Map<String, String> getResponseHeaders() {
return responseHeaders;
}
}
/**
* Information about the URLs used to host the assets in the WebView.
*/
public static class AssetHostingDetails {
private Uri httpPrefix;
private Uri httpsPrefix;
/*package*/ AssetHostingDetails(Uri httpPrefix, Uri httpsPrefix) {
this.httpPrefix = httpPrefix;
this.httpsPrefix = httpsPrefix;
}
/**
* Gets the http: scheme prefix at which assets are hosted.
*
* @return the http: scheme prefix at which assets are hosted. Can return null.
*/
public Uri getHttpPrefix() {
return httpPrefix;
}
/**
* Gets the https: scheme prefix at which assets are hosted.
*
* @return the https: scheme prefix at which assets are hosted. Can return null.
*/
public Uri getHttpsPrefix() {
return httpsPrefix;
}
}
WebViewLocalServer(Context context, String authority, boolean html5mode, ConfigXmlParser parser, String customScheme) {
uriMatcher = new UriMatcher(null);
this.html5mode = html5mode;
this.parser = parser;
this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
this.authority = authority;
this.customScheme = customScheme;
}
private static Uri parseAndVerifyUrl(String url) {
if (url == null) {
return null;
}
Uri uri = Uri.parse(url);
if (uri == null) {
Log.e(TAG, "Malformed URL: " + url);
return null;
}
String path = uri.getPath();
if (path == null || path.length() == 0) {
Log.e(TAG, "URL does not have a path: " + url);
return null;
}
return uri;
}
private static WebResourceResponse createWebResourceResponse(String mimeType, String encoding, int statusCode, String reasonPhrase, Map<String, String> responseHeaders, InputStream data) {
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
int finalStatusCode = statusCode;
try {
if (data.available() == 0) {
finalStatusCode = 404;
}
} catch (IOException e) {
finalStatusCode = 500;
}
return new WebResourceResponse(mimeType, encoding, finalStatusCode, reasonPhrase, responseHeaders, data);
} else {
return new WebResourceResponse(mimeType, encoding, data);
}
}
/**
* Attempt to retrieve the WebResourceResponse associated with the given <code>request</code>.
* This method should be invoked from within
* {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView,
* android.webkit.WebResourceRequest)}.
*
* @param uri the request Uri to process.
* @return a response if the request URL had a matching handler, null if no handler was found.
*/
public WebResourceResponse shouldInterceptRequest(Uri uri, WebResourceRequest request) {
PathHandler handler;
synchronized (uriMatcher) {
handler = (PathHandler) uriMatcher.match(uri);
}
if (handler == null) {
return null;
}
if (isLocalFile(uri) || uri.getAuthority().equals(this.authority)) {
Log.d("SERVER", "Handling local request: " + uri.toString());
return handleLocalRequest(uri, handler, request);
} else {
return handleProxyRequest(uri, handler);
}
}
private boolean isLocalFile(Uri uri) {
String path = uri.getPath();
if (path.startsWith(contentStart) || path.startsWith(fileStart)) {
return true;
}
return false;
}
private WebResourceResponse handleLocalRequest(Uri uri, PathHandler handler, WebResourceRequest request) {
String path = uri.getPath();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && request != null && request.getRequestHeaders().get("Range") != null) {
InputStream responseStream = new LollipopLazyInputStream(handler, uri);
String mimeType = getMimeType(path, responseStream);
Map<String, String> tempResponseHeaders = handler.getResponseHeaders();
int statusCode = 206;
try {
int totalRange = responseStream.available();
String rangeString = request.getRequestHeaders().get("Range");
String[] parts = rangeString.split("=");
String[] streamParts = parts[1].split("-");
String fromRange = streamParts[0];
int range = totalRange-1;
if (streamParts.length > 1) {
range = Integer.parseInt(streamParts[1]);
}
tempResponseHeaders.put("Accept-Ranges", "bytes");
tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange);
} catch (IOException e) {
statusCode = 404;
}
return createWebResourceResponse(mimeType, handler.getEncoding(),
statusCode, handler.getReasonPhrase(), tempResponseHeaders, responseStream);
}
if (isLocalFile(uri)) {
InputStream responseStream = new LollipopLazyInputStream(handler, uri);
String mimeType = getMimeType(path, responseStream);
return createWebResourceResponse(mimeType, handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
}
if (path.equals("") || path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) {
InputStream stream;
String launchURL = parser.getLaunchUrl();
String launchFile = launchURL.substring(launchURL.lastIndexOf("/") + 1, launchURL.length());
try {
String startPath = this.basePath + "/" + launchFile;
if (isAsset) {
stream = protocolHandler.openAsset(startPath);
} else {
stream = protocolHandler.openFile(startPath);
}
} catch (IOException e) {
Log.e(TAG, "Unable to open " + launchFile, e);
return null;
}
return createWebResourceResponse("text/html", handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
}
int periodIndex = path.lastIndexOf(".");
if (periodIndex >= 0) {
InputStream responseStream = new LollipopLazyInputStream(handler, uri);
String mimeType = getMimeType(path, responseStream);
return createWebResourceResponse(mimeType, handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), responseStream);
}
return null;
}
/**
* Instead of reading files from the filesystem/assets, proxy through to the URL
* and let an external server handle it.
* @param uri
* @param handler
* @return
*/
private WebResourceResponse handleProxyRequest(Uri uri, PathHandler handler) {
try {
String path = uri.getPath();
URL url = new URL(uri.toString());
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setReadTimeout(30 * 1000);
conn.setConnectTimeout(30 * 1000);
InputStream stream = conn.getInputStream();
if (path.equals("/") || (!uri.getLastPathSegment().contains(".") && html5mode)) {
return createWebResourceResponse("text/html", handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
}
int periodIndex = path.lastIndexOf(".");
if (periodIndex >= 0) {
String ext = path.substring(path.lastIndexOf("."), path.length());
// TODO: Conjure up a bit more subtlety than this
if (ext.equals(".html")) {
}
String mimeType = getMimeType(path, stream);
return createWebResourceResponse(mimeType, handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
}
return createWebResourceResponse("", handler.getEncoding(),
handler.getStatusCode(), handler.getReasonPhrase(), handler.getResponseHeaders(), stream);
} catch (SocketTimeoutException ex) {
// bridge.handleAppUrlLoadError(ex);
} catch (Exception ex) {
// bridge.handleAppUrlLoadError(ex);
}
return null;
}
private String getMimeType(String path, InputStream stream) {
String mimeType = null;
try {
mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js
if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) {
Log.d(IonicWebViewEngine.TAG, "We shouldn't be here");
}
if (mimeType == null) {
if (path.endsWith(".js") || path.endsWith(".mjs")) {
// Make sure JS files get the proper mimetype to support ES modules
mimeType = "application/javascript";
} else if (path.endsWith(".wasm")) {
mimeType = "application/wasm";
} else {
mimeType = URLConnection.guessContentTypeFromStream(stream);
}
}
} catch (Exception ex) {
Log.e(TAG, "Unable to get mime type" + path, ex);
}
return mimeType;
}
/**
* Registers a handler for the given <code>uri</code>. The <code>handler</code> will be invoked
* every time the <code>shouldInterceptRequest</code> method of the instance is called with
* a matching <code>uri</code>.
*
* @param uri the uri to use the handler for. The scheme and authority (domain) will be matched
* exactly. The path may contain a '*' element which will match a single element of
* a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html
* but not for /a/b/b) or the '**' element which will match any number of path
* elements.
* @param handler the handler to use for the uri.
*/
void register(Uri uri, PathHandler handler) {
synchronized (uriMatcher) {
uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler);
}
}
/**
* Hosts the application's assets on an http(s):// URL. Assets from the local path
* <code>assetPath/...</code> will be available under
* <code>http(s)://{uuid}.androidplatform.net/assets/...</code>.
*
* @param assetPath the local path in the application's asset folder which will be made
* available by the server (for example "/www").
*/
public void hostAssets(String assetPath) {
hostAssets(authority, assetPath);
}
/**
* Hosts the application's assets on an http(s):// URL. Assets from the local path
* <code>assetPath/...</code> will be available under
* <code>http(s)://{domain}/{virtualAssetPath}/...</code>.
*
* @param domain custom domain on which the assets should be hosted (for example "example.com").
* @param assetPath the local path in the application's asset folder which will be made
* available by the server (for example "/www").
* @return prefixes under which the assets are hosted.
*/
public void hostAssets(final String domain,
final String assetPath) {
this.isAsset = true;
this.basePath = assetPath;
createHostingDetails();
}
private void createHostingDetails() {
final String assetPath = this.basePath;
if (assetPath.indexOf('*') != -1) {
throw new IllegalArgumentException("assetPath cannot contain the '*' character.");
}
PathHandler handler = new PathHandler() {
@Override
public InputStream handle(Uri url) {
InputStream stream = null;
String path = url.getPath();
try {
if (path.startsWith(contentStart)) {
stream = protocolHandler.openContentUrl(url);
} else if (path.startsWith(fileStart) || !isAsset) {
if (!path.startsWith(fileStart)) {
path = basePath + url.getPath();
}
stream = protocolHandler.openFile(path);
} else {
stream = protocolHandler.openAsset(assetPath + path);
}
} catch (IOException e) {
Log.e(TAG, "Unable to open asset URL: " + url);
return null;
}
return stream;
}
};
registerUriForScheme(httpScheme, handler, authority);
registerUriForScheme(httpsScheme, handler, authority);
if (!customScheme.equals(httpScheme) && !customScheme.equals(httpsScheme)) {
registerUriForScheme(customScheme, handler, authority);
}
}
private void registerUriForScheme(String scheme, PathHandler handler, String authority) {
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme(scheme);
uriBuilder.authority(authority);
uriBuilder.path("");
Uri uriPrefix = uriBuilder.build();
register(Uri.withAppendedPath(uriPrefix, "/"), handler);
register(Uri.withAppendedPath(uriPrefix, "**"), handler);
}
/**
* Hosts the application's resources on an http(s):// URL. Resources
* <code>http(s)://{uuid}.androidplatform.net/res/{resource_type}/{resource_name}</code>.
*
* @return prefixes under which the resources are hosted.
*/
public AssetHostingDetails hostResources() {
return hostResources(authority, "/res", true, true);
}
/**
* Hosts the application's resources on an http(s):// URL. Resources
* <code>http(s)://{uuid}.androidplatform.net/{virtualResourcesPath}/{resource_type}/{resource_name}</code>.
*
* @param virtualResourcesPath the path on the local server under which the resources
* should be hosted.
* @param enableHttp whether to enable hosting using the http scheme.
* @param enableHttps whether to enable hosting using the https scheme.
* @return prefixes under which the resources are hosted.
*/
public AssetHostingDetails hostResources(final String virtualResourcesPath, boolean enableHttp,
boolean enableHttps) {
return hostResources(authority, virtualResourcesPath, enableHttp, enableHttps);
}
/**
* Hosts the application's resources on an http(s):// URL. Resources
* <code>http(s)://{domain}/{virtualResourcesPath}/{resource_type}/{resource_name}</code>.
*
* @param domain custom domain on which the assets should be hosted (for example "example.com").
* If untrusted content is to be loaded into the WebView it is advised to make
* this random.
* @param virtualResourcesPath the path on the local server under which the resources
* should be hosted.
* @param enableHttp whether to enable hosting using the http scheme.
* @param enableHttps whether to enable hosting using the https scheme.
* @return prefixes under which the resources are hosted.
*/
public AssetHostingDetails hostResources(final String domain,
final String virtualResourcesPath, boolean enableHttp,
boolean enableHttps) {
if (virtualResourcesPath.indexOf('*') != -1) {
throw new IllegalArgumentException(
"virtualResourcesPath cannot contain the '*' character.");
}
Uri.Builder uriBuilder = new Uri.Builder();
uriBuilder.scheme(httpScheme);
uriBuilder.authority(domain);
uriBuilder.path(virtualResourcesPath);
Uri httpPrefix = null;
Uri httpsPrefix = null;
PathHandler handler = new PathHandler() {
@Override
public InputStream handle(Uri url) {
InputStream stream = protocolHandler.openResource(url);
String mimeType = null;
try {
mimeType = URLConnection.guessContentTypeFromStream(stream);
} catch (Exception ex) {
Log.e(TAG, "Unable to get mime type" + url);
}
return stream;
}
};
if (enableHttp) {
httpPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpPrefix, "**"), handler);
}
if (enableHttps) {
uriBuilder.scheme(httpsScheme);
httpsPrefix = uriBuilder.build();
register(Uri.withAppendedPath(httpsPrefix, "**"), handler);
}
return new AssetHostingDetails(httpPrefix, httpsPrefix);
}
/**
* Hosts the application's files on an http(s):// URL. Files from the basePath
* <code>basePath/...</code> will be available under
* <code>http(s)://{uuid}.androidplatform.net/...</code>.
*
* @param basePath the local path in the application's data folder which will be made
* available by the server (for example "/www").
*/
public void hostFiles(final String basePath) {
this.isAsset = false;
this.basePath = basePath;
createHostingDetails();
}
/**
* The KitKat WebView reads the InputStream on a separate threadpool. We can use that to
* parallelize loading.
*/
private static abstract class LazyInputStream extends InputStream {
protected final PathHandler handler;
private InputStream is = null;
public LazyInputStream(PathHandler handler) {
this.handler = handler;
}
private InputStream getInputStream() {
if (is == null) {
is = handle();
}
return is;
}
protected abstract InputStream handle();
@Override
public int available() throws IOException {
InputStream is = getInputStream();
return (is != null) ? is.available() : 0;
}
@Override
public int read() throws IOException {
InputStream is = getInputStream();
return (is != null) ? is.read() : -1;
}
@Override
public int read(byte b[]) throws IOException {
InputStream is = getInputStream();
return (is != null) ? is.read(b) : -1;
}
@Override
public int read(byte b[], int off, int len) throws IOException {
InputStream is = getInputStream();
return (is != null) ? is.read(b, off, len) : -1;
}
@Override
public long skip(long n) throws IOException {
InputStream is = getInputStream();
return (is != null) ? is.skip(n) : 0;
}
}
// For L and above.
private static class LollipopLazyInputStream extends LazyInputStream {
private Uri uri;
private InputStream is;
public LollipopLazyInputStream(PathHandler handler, Uri uri) {
super(handler);
this.uri = uri;
}
@Override
protected InputStream handle() {
return handler.handle(uri);
}
}
public String getBasePath(){
return this.basePath;
}
}
@@ -0,0 +1,165 @@
package io.ionic.keyboard;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.apache.cordova.PluginResult.Status;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.Context;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.inputmethod.InputMethodManager;
// import additionally required classes for calculating screen height
import android.view.Display;
import android.graphics.Point;
import android.os.Build;
import android.widget.FrameLayout;
public class CDVIonicKeyboard extends CordovaPlugin {
private OnGlobalLayoutListener list;
private View rootView;
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
}
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException {
if ("hide".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
//http://stackoverflow.com/a/7696791/1091751
InputMethodManager inputManager = (InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
View v = cordova.getActivity().getCurrentFocus();
if (v == null) {
callbackContext.error("No current focus");
} else {
inputManager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
callbackContext.success(); // Thread-safe.
}
}
});
return true;
}
if ("show".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
((InputMethodManager) cordova.getActivity().getSystemService(Context.INPUT_METHOD_SERVICE)).toggleSoftInput(0, InputMethodManager.HIDE_IMPLICIT_ONLY);
callbackContext.success(); // Thread-safe.
}
});
return true;
}
if ("init".equals(action)) {
cordova.getThreadPool().execute(new Runnable() {
public void run() {
//calculate density-independent pixels (dp)
//http://developer.android.com/guide/practices/screens_support.html
DisplayMetrics dm = new DisplayMetrics();
cordova.getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
final float density = dm.density;
//http://stackoverflow.com/a/4737265/1091751 detect if keyboard is showing
FrameLayout content = (FrameLayout) cordova.getActivity().findViewById(android.R.id.content);
rootView = content.getRootView();
list = new OnGlobalLayoutListener() {
int previousHeightDiff = 0;
@Override
public void onGlobalLayout() {
boolean resize = preferences.getBoolean("resizeOnFullScreen", false);
if (resize) {
possiblyResizeChildOfContent();
}
Rect r = new Rect();
//r will be populated with the coordinates of your view that area still visible.
rootView.getWindowVisibleDisplayFrame(r);
PluginResult result;
// cache properties for later use
int rootViewHeight = rootView.getRootView().getHeight();
int resultBottom = r.bottom;
// calculate screen height differently for android versions >= 21: Lollipop 5.x, Marshmallow 6.x
//http://stackoverflow.com/a/29257533/3642890 beware of nexus 5
int screenHeight;
if (Build.VERSION.SDK_INT >= 21) {
Display display = cordova.getActivity().getWindowManager().getDefaultDisplay();
Point size = new Point();
display.getSize(size);
screenHeight = size.y;
} else {
screenHeight = rootViewHeight;
}
int heightDiff = screenHeight - resultBottom;
int pixelHeightDiff = (int)(heightDiff / density);
if (pixelHeightDiff > 100 && pixelHeightDiff != previousHeightDiff) { // if more than 100 pixels, its probably a keyboard...
String msg = "S" + Integer.toString(pixelHeightDiff);
result = new PluginResult(PluginResult.Status.OK, msg);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}
else if ( pixelHeightDiff != previousHeightDiff && ( previousHeightDiff - pixelHeightDiff ) > 100 ){
String msg = "H";
result = new PluginResult(PluginResult.Status.OK, msg);
result.setKeepCallback(true);
callbackContext.sendPluginResult(result);
}
previousHeightDiff = pixelHeightDiff;
}
private void possiblyResizeChildOfContent() {
int usableHeightNow = computeUsableHeight();
if (usableHeightNow != usableHeightPrevious) {
int usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
if (heightDifference > (usableHeightSansKeyboard/4)) {
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
} else {
frameLayoutParams.height = usableHeightSansKeyboard;
}
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
private int computeUsableHeight() {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
return (r.bottom - r.top);
}
};
mChildOfContent = content.getChildAt(0);
rootView.getViewTreeObserver().addOnGlobalLayoutListener(list);
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
PluginResult dataResult = new PluginResult(PluginResult.Status.OK);
dataResult.setKeepCallback(true);
callbackContext.sendPluginResult(dataResult);
}
});
return true;
}
return false; // Returning false results in a "MethodNotFound" error.
}
@Override
public void onDestroy() {
rootView.getViewTreeObserver().removeOnGlobalLayoutListener(list);
}
}
@@ -0,0 +1,41 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package ir.xFramework.xFrameworkTest;
import android.os.Bundle;
import org.apache.cordova.*;
public class MainActivity extends CordovaActivity
{
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// enable Cordova apps to be started in the background
Bundle extras = getIntent().getExtras();
if (extras != null && extras.getBoolean("cdvStartInBackground", false)) {
moveTaskToBack(true);
}
// Set by <content src="index.html" /> in config.xml
loadUrl(launchUrl);
}
}
@@ -0,0 +1,174 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.device;
import java.util.TimeZone;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaInterface;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.provider.Settings;
public class Device extends CordovaPlugin {
public static final String TAG = "Device";
public static String platform; // Device OS
public static String uuid; // Device UUID
private static final String ANDROID_PLATFORM = "Android";
private static final String AMAZON_PLATFORM = "amazon-fireos";
private static final String AMAZON_DEVICE = "Amazon";
/**
* Constructor.
*/
public Device() {
}
/**
* Sets the context of the Command. This can then be used to do things like
* get file paths associated with the Activity.
*
* @param cordova The context of the main Activity.
* @param webView The CordovaWebView Cordova is running in.
*/
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
Device.uuid = getUuid();
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback id used when calling back into JavaScript.
* @return True if the action was valid, false if not.
*/
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if ("getDeviceInfo".equals(action)) {
JSONObject r = new JSONObject();
r.put("uuid", Device.uuid);
r.put("version", this.getOSVersion());
r.put("platform", this.getPlatform());
r.put("model", this.getModel());
r.put("manufacturer", this.getManufacturer());
r.put("isVirtual", this.isVirtual());
r.put("serial", this.getSerialNumber());
callbackContext.success(r);
}
else {
return false;
}
return true;
}
//--------------------------------------------------------------------------
// LOCAL METHODS
//--------------------------------------------------------------------------
/**
* Get the OS name.
*
* @return
*/
public String getPlatform() {
String platform;
if (isAmazonDevice()) {
platform = AMAZON_PLATFORM;
} else {
platform = ANDROID_PLATFORM;
}
return platform;
}
/**
* Get the device's Universally Unique Identifier (UUID).
*
* @return
*/
public String getUuid() {
String uuid = Settings.Secure.getString(this.cordova.getActivity().getContentResolver(), android.provider.Settings.Secure.ANDROID_ID);
return uuid;
}
public String getModel() {
String model = android.os.Build.MODEL;
return model;
}
public String getProductName() {
String productname = android.os.Build.PRODUCT;
return productname;
}
public String getManufacturer() {
String manufacturer = android.os.Build.MANUFACTURER;
return manufacturer;
}
public String getSerialNumber() {
String serial = android.os.Build.SERIAL;
return serial;
}
/**
* Get the OS version.
*
* @return
*/
public String getOSVersion() {
String osversion = android.os.Build.VERSION.RELEASE;
return osversion;
}
public String getSDKVersion() {
@SuppressWarnings("deprecation")
String sdkversion = android.os.Build.VERSION.SDK;
return sdkversion;
}
public String getTimeZoneID() {
TimeZone tz = TimeZone.getDefault();
return (tz.getID());
}
/**
* Function to check if the device is manufactured by Amazon
*
* @return
*/
public boolean isAmazonDevice() {
if (android.os.Build.MANUFACTURER.equals(AMAZON_DEVICE)) {
return true;
}
return false;
}
public boolean isVirtual() {
return android.os.Build.FINGERPRINT.contains("generic") ||
android.os.Build.PRODUCT.contains("sdk");
}
}
@@ -0,0 +1,413 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.splashscreen;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Configuration;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Handler;
import android.view.Display;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.view.WindowManager;
import android.view.animation.Animation;
import android.view.animation.AlphaAnimation;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.json.JSONArray;
import org.json.JSONException;
public class SplashScreen extends CordovaPlugin {
private static final String LOG_TAG = "SplashScreen";
// Cordova 3.x.x has a copy of this plugin bundled with it (SplashScreenInternal.java).
// Enable functionality only if running on 4.x.x.
private static final boolean HAS_BUILT_IN_SPLASH_SCREEN = Integer.valueOf(CordovaWebView.CORDOVA_VERSION.split("\\.")[0]) < 4;
private static final int DEFAULT_SPLASHSCREEN_DURATION = 3000;
private static final int DEFAULT_FADE_DURATION = 500;
private static Dialog splashDialog;
private static ProgressDialog spinnerDialog;
private static boolean firstShow = true;
private static boolean lastHideAfterDelay; // https://issues.apache.org/jira/browse/CB-9094
/**
* Displays the splash drawable.
*/
private ImageView splashImageView;
/**
* Remember last device orientation to detect orientation changes.
*/
private int orientation;
// Helper to be compile-time compatible with both Cordova 3.x and 4.x.
private View getView() {
try {
return (View)webView.getClass().getMethod("getView").invoke(webView);
} catch (Exception e) {
return (View)webView;
}
}
private int getSplashId() {
int drawableId = 0;
String splashResource = preferences.getString("SplashScreen", "screen");
if (splashResource != null) {
drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getClass().getPackage().getName());
if (drawableId == 0) {
drawableId = cordova.getActivity().getResources().getIdentifier(splashResource, "drawable", cordova.getActivity().getPackageName());
}
}
return drawableId;
}
@Override
protected void pluginInitialize() {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// Make WebView invisible while loading URL
// CB-11326 Ensure we're calling this on UI thread
cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
getView().setVisibility(View.INVISIBLE);
}
});
int drawableId = getSplashId();
// Save initial orientation.
orientation = cordova.getActivity().getResources().getConfiguration().orientation;
if (firstShow) {
boolean autoHide = preferences.getBoolean("AutoHideSplashScreen", true);
showSplashScreen(autoHide);
}
if (preferences.getBoolean("SplashShowOnlyFirstTime", true)) {
firstShow = false;
}
}
/**
* Shorter way to check value of "SplashMaintainAspectRatio" preference.
*/
private boolean isMaintainAspectRatio () {
return preferences.getBoolean("SplashMaintainAspectRatio", false);
}
private int getFadeDuration () {
int fadeSplashScreenDuration = preferences.getBoolean("FadeSplashScreen", true) ?
preferences.getInteger("FadeSplashScreenDuration", DEFAULT_FADE_DURATION) : 0;
if (fadeSplashScreenDuration < 30) {
// [CB-9750] This value used to be in decimal seconds, so we will assume that if someone specifies 10
// they mean 10 seconds, and not the meaningless 10ms
fadeSplashScreenDuration *= 1000;
}
return fadeSplashScreenDuration;
}
@Override
public void onPause(boolean multitasking) {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// hide the splash screen to avoid leaking a window
this.removeSplashScreen(true);
}
@Override
public void onDestroy() {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return;
}
// hide the splash screen to avoid leaking a window
this.removeSplashScreen(true);
// If we set this to true onDestroy, we lose track when we go from page to page!
//firstShow = true;
}
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
if (action.equals("hide")) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.postMessage("splashscreen", "hide");
}
});
} else if (action.equals("show")) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
webView.postMessage("splashscreen", "show");
}
});
} else {
return false;
}
callbackContext.success();
return true;
}
@Override
public Object onMessage(String id, Object data) {
if (HAS_BUILT_IN_SPLASH_SCREEN) {
return null;
}
if ("splashscreen".equals(id)) {
if ("hide".equals(data.toString())) {
this.removeSplashScreen(false);
} else {
this.showSplashScreen(false);
}
} else if ("spinner".equals(id)) {
if ("stop".equals(data.toString())) {
getView().setVisibility(View.VISIBLE);
}
} else if ("onReceivedError".equals(id)) {
this.spinnerStop();
}
return null;
}
// Don't add @Override so that plugin still compiles on 3.x.x for a while
public void onConfigurationChanged(Configuration newConfig) {
if (newConfig.orientation != orientation) {
orientation = newConfig.orientation;
// Splash drawable may change with orientation, so reload it.
if (splashImageView != null) {
int drawableId = getSplashId();
if (drawableId != 0) {
splashImageView.setImageDrawable(cordova.getActivity().getResources().getDrawable(drawableId));
}
}
}
}
private void removeSplashScreen(final boolean forceHideImmediately) {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (splashDialog != null && splashDialog.isShowing()) {
final int fadeSplashScreenDuration = getFadeDuration();
// CB-10692 If the plugin is being paused/destroyed, skip the fading and hide it immediately
if (fadeSplashScreenDuration > 0 && forceHideImmediately == false) {
AlphaAnimation fadeOut = new AlphaAnimation(1, 0);
fadeOut.setInterpolator(new DecelerateInterpolator());
fadeOut.setDuration(fadeSplashScreenDuration);
splashImageView.setAnimation(fadeOut);
splashImageView.startAnimation(fadeOut);
fadeOut.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
spinnerStop();
}
@Override
public void onAnimationEnd(Animation animation) {
if (splashDialog != null && splashDialog.isShowing()) {
splashDialog.dismiss();
splashDialog = null;
splashImageView = null;
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
} else {
spinnerStop();
splashDialog.dismiss();
splashDialog = null;
splashImageView = null;
}
}
}
});
}
/**
* Shows the splash screen over the full Activity
*/
@SuppressWarnings("deprecation")
private void showSplashScreen(final boolean hideAfterDelay) {
final int splashscreenTime = preferences.getInteger("SplashScreenDelay", DEFAULT_SPLASHSCREEN_DURATION);
final int drawableId = getSplashId();
final int fadeSplashScreenDuration = getFadeDuration();
final int effectiveSplashDuration = Math.max(0, splashscreenTime - fadeSplashScreenDuration);
lastHideAfterDelay = hideAfterDelay;
// Prevent to show the splash dialog if the activity is in the process of finishing
if (cordova.getActivity().isFinishing()) {
return;
}
// If the splash dialog is showing don't try to show it again
if (splashDialog != null && splashDialog.isShowing()) {
return;
}
if (drawableId == 0 || (splashscreenTime <= 0 && hideAfterDelay)) {
return;
}
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
// Get reference to display
Display display = cordova.getActivity().getWindowManager().getDefaultDisplay();
Context context = webView.getContext();
// Use an ImageView to render the image because of its flexible scaling options.
splashImageView = new ImageView(context);
splashImageView.setImageResource(drawableId);
LayoutParams layoutParams = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
splashImageView.setLayoutParams(layoutParams);
splashImageView.setMinimumHeight(display.getHeight());
splashImageView.setMinimumWidth(display.getWidth());
// TODO: Use the background color of the webView's parent instead of using the preference.
splashImageView.setBackgroundColor(preferences.getInteger("backgroundColor", Color.BLACK));
if (isMaintainAspectRatio()) {
// CENTER_CROP scale mode is equivalent to CSS "background-size:cover"
splashImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
}
else {
// FIT_XY scales image non-uniformly to fit into image view.
splashImageView.setScaleType(ImageView.ScaleType.FIT_XY);
}
// Create and show the dialog
splashDialog = new Dialog(context, android.R.style.Theme_Translucent_NoTitleBar);
// check to see if the splash screen should be full screen
if ((cordova.getActivity().getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN)
== WindowManager.LayoutParams.FLAG_FULLSCREEN) {
splashDialog.getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
splashDialog.setContentView(splashImageView);
splashDialog.setCancelable(false);
splashDialog.show();
if (preferences.getBoolean("ShowSplashScreenSpinner", true)) {
spinnerStart();
}
// Set Runnable to remove splash screen just in case
if (hideAfterDelay) {
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
if (lastHideAfterDelay) {
removeSplashScreen(false);
}
}
}, effectiveSplashDuration);
}
}
});
}
// Show only spinner in the center of the screen
private void spinnerStart() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
spinnerStop();
spinnerDialog = new ProgressDialog(webView.getContext());
spinnerDialog.setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
spinnerDialog = null;
}
});
spinnerDialog.setCancelable(false);
spinnerDialog.setIndeterminate(true);
RelativeLayout centeredLayout = new RelativeLayout(cordova.getActivity());
centeredLayout.setGravity(Gravity.CENTER);
centeredLayout.setLayoutParams(new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
ProgressBar progressBar = new ProgressBar(webView.getContext());
RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE);
progressBar.setLayoutParams(layoutParams);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
String colorName = preferences.getString("SplashScreenSpinnerColor", null);
if(colorName != null){
int[][] states = new int[][] {
new int[] { android.R.attr.state_enabled}, // enabled
new int[] {-android.R.attr.state_enabled}, // disabled
new int[] {-android.R.attr.state_checked}, // unchecked
new int[] { android.R.attr.state_pressed} // pressed
};
int progressBarColor = Color.parseColor(colorName);
int[] colors = new int[] {
progressBarColor,
progressBarColor,
progressBarColor,
progressBarColor
};
ColorStateList colorStateList = new ColorStateList(states, colors);
progressBar.setIndeterminateTintList(colorStateList);
}
}
centeredLayout.addView(progressBar);
spinnerDialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
spinnerDialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
spinnerDialog.show();
spinnerDialog.setContentView(centeredLayout);
}
});
}
private void spinnerStop() {
cordova.getActivity().runOnUiThread(new Runnable() {
public void run() {
if (spinnerDialog != null && spinnerDialog.isShowing()) {
spinnerDialog.dismiss();
spinnerDialog = null;
}
}
});
}
}
@@ -0,0 +1,276 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.cordova.statusbar;
import android.app.Activity;
import android.graphics.Color;
import android.os.Build;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaArgs;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.LOG;
import org.apache.cordova.PluginResult;
import org.json.JSONException;
import java.util.Arrays;
public class StatusBar extends CordovaPlugin {
private static final String TAG = "StatusBar";
/**
* Sets the context of the Command. This can then be used to do things like
* get file paths associated with the Activity.
*
* @param cordova The context of the main Activity.
* @param webView The CordovaWebView Cordova is running in.
*/
@Override
public void initialize(final CordovaInterface cordova, CordovaWebView webView) {
LOG.v(TAG, "StatusBar: initialization");
super.initialize(cordova, webView);
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// Clear flag FLAG_FORCE_NOT_FULLSCREEN which is set initially
// by the Cordova.
Window window = cordova.getActivity().getWindow();
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
// Read 'StatusBarBackgroundColor' from config.xml, default is #000000.
setStatusBarBackgroundColor(preferences.getString("StatusBarBackgroundColor", "#000000"));
// Read 'StatusBarStyle' from config.xml, default is 'lightcontent'.
setStatusBarStyle(preferences.getString("StatusBarStyle", "lightcontent"));
}
});
}
/**
* Executes the request and returns PluginResult.
*
* @param action The action to execute.
* @param args JSONArry of arguments for the plugin.
* @param callbackContext The callback id used when calling back into JavaScript.
* @return True if the action was valid, false otherwise.
*/
@Override
public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
LOG.v(TAG, "Executing action: " + action);
final Activity activity = this.cordova.getActivity();
final Window window = activity.getWindow();
if ("_ready".equals(action)) {
boolean statusBarVisible = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0;
callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, statusBarVisible));
return true;
}
if ("show".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
// use KitKat here to be aligned with "Fullscreen" preference
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = window.getDecorView().getSystemUiVisibility();
uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
window.getDecorView().setSystemUiVisibility(uiOptions);
}
// CB-11197 We still need to update LayoutParams to force status bar
// to be hidden when entering e.g. text fields
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
});
return true;
}
if ("hide".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
// SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we
// use KitKat here to be aligned with "Fullscreen" preference
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int uiOptions = window.getDecorView().getSystemUiVisibility()
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_FULLSCREEN;
window.getDecorView().setSystemUiVisibility(uiOptions);
}
// CB-11197 We still need to update LayoutParams to force status bar
// to be hidden when entering e.g. text fields
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
}
});
return true;
}
if ("backgroundColorByHexString".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
setStatusBarBackgroundColor(args.getString(0));
} catch (JSONException ignore) {
LOG.e(TAG, "Invalid hexString argument, use f.i. '#777777'");
}
}
});
return true;
}
if ("overlaysWebView".equals(action)) {
if (Build.VERSION.SDK_INT >= 21) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
try {
setStatusBarTransparent(args.getBoolean(0));
} catch (JSONException ignore) {
LOG.e(TAG, "Invalid boolean argument");
}
}
});
return true;
}
else return args.getBoolean(0) == false;
}
if ("styleDefault".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("default");
}
});
return true;
}
if ("styleLightContent".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("lightcontent");
}
});
return true;
}
if ("styleBlackTranslucent".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("blacktranslucent");
}
});
return true;
}
if ("styleBlackOpaque".equals(action)) {
this.cordova.getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
setStatusBarStyle("blackopaque");
}
});
return true;
}
return false;
}
private void setStatusBarBackgroundColor(final String colorPref) {
if (Build.VERSION.SDK_INT >= 21) {
if (colorPref != null && !colorPref.isEmpty()) {
final Window window = cordova.getActivity().getWindow();
// Method and constants not available on all SDKs but we want to be able to compile this code with any SDK
window.clearFlags(0x04000000); // SDK 19: WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
window.addFlags(0x80000000); // SDK 21: WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
try {
// Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21
window.getClass().getMethod("setStatusBarColor", int.class).invoke(window, Color.parseColor(colorPref));
} catch (IllegalArgumentException ignore) {
LOG.e(TAG, "Invalid hexString argument, use f.i. '#999999'");
} catch (Exception ignore) {
// this should not happen, only in case Android removes this method in a version > 21
LOG.w(TAG, "Method window.setStatusBarColor not found for SDK level " + Build.VERSION.SDK_INT);
}
}
}
}
private void setStatusBarTransparent(final boolean transparent) {
if (Build.VERSION.SDK_INT >= 21) {
final Window window = cordova.getActivity().getWindow();
if (transparent) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
window.setStatusBarColor(Color.TRANSPARENT);
}
else {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_VISIBLE);
}
}
}
private void setStatusBarStyle(final String style) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (style != null && !style.isEmpty()) {
View decorView = cordova.getActivity().getWindow().getDecorView();
int uiOptions = decorView.getSystemUiVisibility();
String[] darkContentStyles = {
"default",
};
String[] lightContentStyles = {
"lightcontent",
"blacktranslucent",
"blackopaque",
};
if (Arrays.asList(darkContentStyles).contains(style.toLowerCase())) {
decorView.setSystemUiVisibility(uiOptions | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
return;
}
if (Arrays.asList(lightContentStyles).contains(style.toLowerCase())) {
decorView.setSystemUiVisibility(uiOptions & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
return;
}
LOG.e(TAG, "Invalid style, must be either 'default', 'lightcontent' or the deprecated 'blacktranslucent' and 'blackopaque'");
}
}
}
}
@@ -0,0 +1,161 @@
/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
*/
package org.apache.cordova.whitelist;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.ConfigXmlParser;
import org.apache.cordova.LOG;
import org.apache.cordova.Whitelist;
import org.xmlpull.v1.XmlPullParser;
import android.content.Context;
public class WhitelistPlugin extends CordovaPlugin {
private static final String LOG_TAG = "WhitelistPlugin";
private Whitelist allowedNavigations;
private Whitelist allowedIntents;
private Whitelist allowedRequests;
// Used when instantiated via reflection by PluginManager
public WhitelistPlugin() {
}
// These can be used by embedders to allow Java-configuration of whitelists.
public WhitelistPlugin(Context context) {
this(new Whitelist(), new Whitelist(), null);
new CustomConfigXmlParser().parse(context);
}
public WhitelistPlugin(XmlPullParser xmlParser) {
this(new Whitelist(), new Whitelist(), null);
new CustomConfigXmlParser().parse(xmlParser);
}
public WhitelistPlugin(Whitelist allowedNavigations, Whitelist allowedIntents, Whitelist allowedRequests) {
if (allowedRequests == null) {
allowedRequests = new Whitelist();
allowedRequests.addWhiteListEntry("file:///*", false);
allowedRequests.addWhiteListEntry("data:*", false);
}
this.allowedNavigations = allowedNavigations;
this.allowedIntents = allowedIntents;
this.allowedRequests = allowedRequests;
}
@Override
public void pluginInitialize() {
if (allowedNavigations == null) {
allowedNavigations = new Whitelist();
allowedIntents = new Whitelist();
allowedRequests = new Whitelist();
new CustomConfigXmlParser().parse(webView.getContext());
}
}
private class CustomConfigXmlParser extends ConfigXmlParser {
@Override
public void handleStartTag(XmlPullParser xml) {
String strNode = xml.getName();
if (strNode.equals("content")) {
String startPage = xml.getAttributeValue(null, "src");
allowedNavigations.addWhiteListEntry(startPage, false);
} else if (strNode.equals("allow-navigation")) {
String origin = xml.getAttributeValue(null, "href");
if ("*".equals(origin)) {
allowedNavigations.addWhiteListEntry("http://*/*", false);
allowedNavigations.addWhiteListEntry("https://*/*", false);
allowedNavigations.addWhiteListEntry("data:*", false);
} else {
allowedNavigations.addWhiteListEntry(origin, false);
}
} else if (strNode.equals("allow-intent")) {
String origin = xml.getAttributeValue(null, "href");
allowedIntents.addWhiteListEntry(origin, false);
} else if (strNode.equals("access")) {
String origin = xml.getAttributeValue(null, "origin");
String subdomains = xml.getAttributeValue(null, "subdomains");
boolean external = (xml.getAttributeValue(null, "launch-external") != null);
if (origin != null) {
if (external) {
LOG.w(LOG_TAG, "Found <access launch-external> within config.xml. Please use <allow-intent> instead.");
allowedIntents.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
} else {
if ("*".equals(origin)) {
allowedRequests.addWhiteListEntry("http://*/*", false);
allowedRequests.addWhiteListEntry("https://*/*", false);
} else {
allowedRequests.addWhiteListEntry(origin, (subdomains != null) && (subdomains.compareToIgnoreCase("true") == 0));
}
}
}
}
}
@Override
public void handleEndTag(XmlPullParser xml) {
}
}
@Override
public Boolean shouldAllowNavigation(String url) {
if (allowedNavigations.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
@Override
public Boolean shouldAllowRequest(String url) {
if (Boolean.TRUE == shouldAllowNavigation(url)) {
return true;
}
if (allowedRequests.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
@Override
public Boolean shouldOpenExternalUrl(String url) {
if (allowedIntents.isUrlWhiteListed(url)) {
return true;
}
return null; // Default policy
}
public Whitelist getAllowedNavigations() {
return allowedNavigations;
}
public void setAllowedNavigations(Whitelist allowedNavigations) {
this.allowedNavigations = allowedNavigations;
}
public Whitelist getAllowedIntents() {
return allowedIntents;
}
public void setAllowedIntents(Whitelist allowedIntents) {
this.allowedIntents = allowedIntents;
}
public Whitelist getAllowedRequests() {
return allowedRequests;
}
public void setAllowedRequests(Whitelist allowedRequests) {
this.allowedRequests = allowedRequests;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

@@ -0,0 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">xFrameworkTest</string>
<string name="launcher_name">@string/app_name</string>
<string name="activity_name">@string/launcher_name</string>
</resources>
@@ -0,0 +1,70 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="ir.xFramework.xFrameworkTest" version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<feature name="Device">
<param name="android-package" value="org.apache.cordova.device.Device" />
</feature>
<feature name="SplashScreen">
<param name="android-package" value="org.apache.cordova.splashscreen.SplashScreen" />
<param name="onload" value="true" />
</feature>
<feature name="StatusBar">
<param name="android-package" value="org.apache.cordova.statusbar.StatusBar" />
<param name="onload" value="true" />
</feature>
<feature name="Whitelist">
<param name="android-package" value="org.apache.cordova.whitelist.WhitelistPlugin" />
<param name="onload" value="true" />
</feature>
<feature name="CDVIonicKeyboard">
<param name="android-package" onload="true" value="io.ionic.keyboard.CDVIonicKeyboard" />
</feature>
<allow-navigation href="http://localhost/*" />
<allow-navigation href="https://localhost/*" />
<allow-navigation href="ionic://*" />
<feature name="IonicWebView">
<param name="android-package" value="com.ionicframework.cordova.webview.IonicWebView" />
</feature>
<name>xFrameworkTest</name>
<description>test all xFramework capabilities</description>
<author email="hadi_khazaee_asl@yahoo.com" href="https://x-framework.ir">Hadi Khazaee Asl</author>
<content src="index.html" />
<access origin="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
<allow-intent href="tel:*" />
<allow-intent href="sms:*" />
<allow-intent href="mailto:*" />
<allow-intent href="geo:*" />
<edit-config file="app/src/main/AndroidManifest.xml" mode="merge" target="/manifest/application" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:networkSecurityConfig="@xml/network_security_config" />
</edit-config>
<resource-file src="resources/android/xml/network_security_config.xml" target="app/src/main/res/xml/network_security_config.xml" />
<allow-intent href="market:*" />
<icon density="ldpi" src="resources/android/icon/drawable-ldpi-icon.png" />
<icon density="mdpi" src="resources/android/icon/drawable-mdpi-icon.png" />
<icon density="hdpi" src="resources/android/icon/drawable-hdpi-icon.png" />
<icon density="xhdpi" src="resources/android/icon/drawable-xhdpi-icon.png" />
<icon density="xxhdpi" src="resources/android/icon/drawable-xxhdpi-icon.png" />
<icon density="xxxhdpi" src="resources/android/icon/drawable-xxxhdpi-icon.png" />
<splash density="land-ldpi" src="resources/android/splash/drawable-land-ldpi-screen.png" />
<splash density="land-mdpi" src="resources/android/splash/drawable-land-mdpi-screen.png" />
<splash density="land-hdpi" src="resources/android/splash/drawable-land-hdpi-screen.png" />
<splash density="land-xhdpi" src="resources/android/splash/drawable-land-xhdpi-screen.png" />
<splash density="land-xxhdpi" src="resources/android/splash/drawable-land-xxhdpi-screen.png" />
<splash density="land-xxxhdpi" src="resources/android/splash/drawable-land-xxxhdpi-screen.png" />
<splash density="port-ldpi" src="resources/android/splash/drawable-port-ldpi-screen.png" />
<splash density="port-mdpi" src="resources/android/splash/drawable-port-mdpi-screen.png" />
<splash density="port-hdpi" src="resources/android/splash/drawable-port-hdpi-screen.png" />
<splash density="port-xhdpi" src="resources/android/splash/drawable-port-xhdpi-screen.png" />
<splash density="port-xxhdpi" src="resources/android/splash/drawable-port-xxhdpi-screen.png" />
<splash density="port-xxxhdpi" src="resources/android/splash/drawable-port-xxxhdpi-screen.png" />
<preference name="loglevel" value="DEBUG" />
<preference name="webView" value="com.ionicframework.cordova.webview.IonicWebViewEngine" />
<preference name="ScrollEnabled" value="false" />
<preference name="BackupWebStorage" value="none" />
<preference name="SplashMaintainAspectRatio" value="true" />
<preference name="FadeSplashScreenDuration" value="300" />
<preference name="SplashShowOnlyFirstTime" value="false" />
<preference name="SplashScreen" value="screen" />
<preference name="SplashScreenDelay" value="3000" />
</widget>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
</domain-config>
</network-security-config>