Initial Commit on git.saherelm.ir ...
@@ -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;
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 1.0 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 432 KiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 920 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
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>
|
||||