Attempt to get FDroid build working
This commit is contained in:
parent
268f1b87e8
commit
5b4e9fa452
77 changed files with 11993 additions and 1 deletions
36
README.md
36
README.md
|
@ -2,3 +2,39 @@
|
|||
All the tooling and scripts needed to package the mapcomplete.org webapp into an Android shell.
|
||||
|
||||
_Please, report bugs in the [main MapComplete repository](https://source.mapcomplete.org/MapComplete/MapComplete/issues)_
|
||||
|
||||
|
||||
## To update on F-Droid
|
||||
|
||||
(These are notes for the maintainer)
|
||||
|
||||
- To update "capacitor-android", copy the 'android'-directory from "(mapcomplete repo)/node_modules/@capacitor/android" into the adroid repo root
|
||||
The path should be linked in capacitor.settings.gradle
|
||||
|
||||
- In the `fdroiddata`-repostiroy, open `metadata/org.mapcomplete.yml`
|
||||
- Update the version number to match the MC release
|
||||
- Bump the `CurrentVersionCode` with one
|
||||
|
||||
Then, test the build (instructions adapted from [FDroids quick start guide](https://f-droid.org/en/docs/Submitting_to_F-Droid_Quick_Start_Guide/)):
|
||||
- Go to the `fdroidserver` rep in a terminal
|
||||
- Start the container with:
|
||||
```
|
||||
sudo docker run --rm -itu vagrant --entrypoint /bin/bash \
|
||||
-v ~/fdroiddata:/build:z \
|
||||
-v ~/fdroidserver:/home/vagrant/fdroidserver:Z \
|
||||
registry.gitlab.com/fdroid/fdroidserver:buildserver
|
||||
```
|
||||
|
||||
In the container, run:
|
||||
|
||||
```
|
||||
. /etc/profile
|
||||
export PATH="$fdroidserver:$PATH" PYTHONPATH="$fdroidserver"
|
||||
export JAVA_HOME=$(java -XshowSettings:properties -version 2>&1 > /dev/null | grep 'java.home' | awk -F'=' '{print $2}' | tr -d ' ')
|
||||
cd /build
|
||||
fdroid readmeta
|
||||
fdroid rewritemeta org.mapcomplete
|
||||
fdroid checkupdates --allow-dirty org.mapcomplete
|
||||
fdroid lint org.mapcomplete
|
||||
fdroid build org.mapcomplete
|
||||
```
|
||||
|
|
21
capacitor-android/LICENSE
Normal file
21
capacitor-android/LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2017-present Drifty Co.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
96
capacitor-android/capacitor/build.gradle
Normal file
96
capacitor-android/capacitor/build.gradle
Normal file
|
@ -0,0 +1,96 @@
|
|||
ext {
|
||||
androidxActivityVersion = project.hasProperty('androidxActivityVersion') ? rootProject.ext.androidxActivityVersion : '1.8.0'
|
||||
androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.2.0'
|
||||
androidxCoreVersion = project.hasProperty('androidxCoreVersion') ? rootProject.ext.androidxCoreVersion : '1.12.0'
|
||||
androidxFragmentVersion = project.hasProperty('androidxFragmentVersion') ? rootProject.ext.androidxFragmentVersion : '1.6.2'
|
||||
androidxWebkitVersion = project.hasProperty('androidxWebkitVersion') ? rootProject.ext.androidxWebkitVersion : '1.9.0'
|
||||
junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
|
||||
androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5'
|
||||
androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1'
|
||||
cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1'
|
||||
}
|
||||
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.9.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
|
||||
if (System.getenv("CAP_PUBLISH") == "true") {
|
||||
classpath 'io.github.gradle-nexus:publish-plugin:1.3.0'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType(Javadoc).all { enabled = false }
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
if (System.getenv("CAP_PUBLISH") == "true") {
|
||||
apply plugin: 'io.github.gradle-nexus.publish-plugin'
|
||||
apply from: file('../scripts/publish-root.gradle')
|
||||
apply from: file('../scripts/publish-module.gradle')
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.getcapacitor.android"
|
||||
compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34
|
||||
defaultConfig {
|
||||
minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22
|
||||
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
consumerProguardFiles 'proguard-rules.pro'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
lintOptions {
|
||||
baseline file("lint-baseline.xml")
|
||||
abortOnError true
|
||||
warningsAsErrors true
|
||||
lintConfig file('lint.xml')
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_17
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
publishing {
|
||||
singleVariant("release")
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version")
|
||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||
implementation "androidx.core:core:$androidxCoreVersion"
|
||||
implementation "androidx.activity:activity:$androidxActivityVersion"
|
||||
implementation "androidx.fragment:fragment:$androidxFragmentVersion"
|
||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||
implementation "androidx.webkit:webkit:$androidxWebkitVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||
implementation "org.apache.cordova:framework:$cordovaAndroidVersion"
|
||||
testImplementation 'org.json:json:20231013'
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
}
|
||||
|
136
capacitor-android/capacitor/lint-baseline.xml
Normal file
136
capacitor-android/capacitor/lint-baseline.xml
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<issues format="5" by="lint 4.1.1" client="gradle" variant="all" version="4.1.1">
|
||||
|
||||
<issue
|
||||
id="DefaultLocale"
|
||||
message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead"
|
||||
errorLine1=" String msg = String.format("
|
||||
errorLine2=" ^">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
|
||||
line="474"
|
||||
column="26"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DefaultLocale"
|
||||
message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||
errorLine1=" return mask.toUpperCase().equals(string.toUpperCase());"
|
||||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/util/HostMask.java"
|
||||
line="110"
|
||||
column="29"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DefaultLocale"
|
||||
message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||
errorLine1=" return mask.toUpperCase().equals(string.toUpperCase());"
|
||||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/util/HostMask.java"
|
||||
line="110"
|
||||
column="57"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DefaultLocale"
|
||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||
errorLine1=" switch (spinnerStyle.toLowerCase()) {"
|
||||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/Splash.java"
|
||||
line="127"
|
||||
column="38"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="DefaultLocale"
|
||||
message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`."
|
||||
errorLine1=" if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {"
|
||||
errorLine2=" ~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/WebViewLocalServer.java"
|
||||
line="327"
|
||||
column="89"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SimpleDateFormat"
|
||||
message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
|
||||
errorLine1=" String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java"
|
||||
line="511"
|
||||
column="28"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SimpleDateFormat"
|
||||
message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates."
|
||||
errorLine1=" DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/PluginResult.java"
|
||||
line="44"
|
||||
column="25"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="SetJavaScriptEnabled"
|
||||
message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully"
|
||||
errorLine1=" settings.setJavaScriptEnabled(true);"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/Bridge.java"
|
||||
line="384"
|
||||
column="9"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `TypedArray` should be recycled after use with `#recycle()`"
|
||||
errorLine1=" TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);"
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/BridgeFragment.java"
|
||||
line="84"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StaticFieldLeak"
|
||||
message="Do not place Android context classes in static fields; this is a memory leak"
|
||||
errorLine1=" private static ImageView splashImage;"
|
||||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/Splash.java"
|
||||
line="41"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="StaticFieldLeak"
|
||||
message="Do not place Android context classes in static fields; this is a memory leak"
|
||||
errorLine1=" private static ProgressBar spinnerBar;"
|
||||
errorLine2=" ~~~~~~">
|
||||
<location
|
||||
file="src/main/java/com/getcapacitor/Splash.java"
|
||||
line="42"
|
||||
column="13"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Overdraw"
|
||||
message="Possible overdraw: Root element paints background `#F0FF1414` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
|
||||
errorLine1=" android:background="#F0FF1414""
|
||||
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
|
||||
<location
|
||||
file="src/main/res/layout/fragment_bridge.xml"
|
||||
line="5"
|
||||
column="5"/>
|
||||
</issue>
|
||||
|
||||
</issues>
|
9
capacitor-android/capacitor/lint.xml
Normal file
9
capacitor-android/capacitor/lint.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<lint>
|
||||
<issue id="GradleDependency" severity="ignore" />
|
||||
<issue id="AndroidGradlePluginVersion" severity="ignore" />
|
||||
<issue id="DiscouragedApi">
|
||||
<ignore path="src/main/java/com/getcapacitor/plugin/util/AssetUtil.java" />
|
||||
</issue>
|
||||
<issue id="ObsoleteSdkInt" severity="informational" />
|
||||
</lint>
|
28
capacitor-android/capacitor/proguard-rules.pro
vendored
Normal file
28
capacitor-android/capacitor/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,28 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# Rules for Capacitor v3 plugins and annotations
|
||||
-keep @com.getcapacitor.annotation.CapacitorPlugin public class * {
|
||||
@com.getcapacitor.annotation.PermissionCallback <methods>;
|
||||
@com.getcapacitor.annotation.ActivityCallback <methods>;
|
||||
@com.getcapacitor.annotation.Permission <methods>;
|
||||
@com.getcapacitor.PluginMethod public <methods>;
|
||||
}
|
||||
|
||||
-keep public class * extends com.getcapacitor.Plugin { *; }
|
||||
|
||||
# Rules for Capacitor v2 plugins and annotations
|
||||
# These are deprecated but can still be used with Capacitor for now
|
||||
-keep @com.getcapacitor.NativePlugin public class * {
|
||||
@com.getcapacitor.PluginMethod public <methods>;
|
||||
}
|
||||
|
||||
# Rules for Cordova plugins
|
||||
-keep public class * extends org.apache.cordova.* {
|
||||
public <methods>;
|
||||
public <fields>;
|
||||
}
|
3
capacitor-android/capacitor/src/main/AndroidManifest.xml
Normal file
3
capacitor-android/capacitor/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
</manifest>
|
1025
capacitor-android/capacitor/src/main/assets/native-bridge.js
Normal file
1025
capacitor-android/capacitor/src/main/assets/native-bridge.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,94 @@
|
|||
// 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.
|
||||
|
||||
package com.getcapacitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.net.Uri;
|
||||
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 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 {
|
||||
Logger.error("Asset not of type string: " + uri);
|
||||
}
|
||||
} catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) {
|
||||
Logger.error("Unable to open resource URL: " + uri, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
return field.getInt(null);
|
||||
}
|
||||
|
||||
public InputStream openFile(String filePath) throws IOException {
|
||||
String realPath = filePath.replace(Bridge.CAPACITOR_FILE_START, "");
|
||||
File localFile = new File(realPath);
|
||||
return new FileInputStream(localFile);
|
||||
}
|
||||
|
||||
public InputStream openContentUrl(Uri uri) throws IOException {
|
||||
Integer port = uri.getPort();
|
||||
String baseUrl = uri.getScheme() + "://" + uri.getHost();
|
||||
if (port != -1) {
|
||||
baseUrl += ":" + port;
|
||||
}
|
||||
String realPath = uri.toString().replace(baseUrl + Bridge.CAPACITOR_CONTENT_START, "content:/");
|
||||
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(Uri.parse(realPath));
|
||||
} catch (SecurityException e) {
|
||||
Logger.error("Unable to open content URL: " + uri, e);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
private static int getValueType(Context context, int fieldId) {
|
||||
TypedValue value = new TypedValue();
|
||||
context.getResources().getValue(fieldId, value, true);
|
||||
return value.type;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class App {
|
||||
|
||||
/**
|
||||
* Interface for callbacks when app status changes.
|
||||
*/
|
||||
public interface AppStatusChangeListener {
|
||||
void onAppStatusChanged(Boolean isActive);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for callbacks when app is restored with pending plugin call.
|
||||
*/
|
||||
public interface AppRestoredListener {
|
||||
void onAppRestored(PluginResult result);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private AppStatusChangeListener statusChangeListener;
|
||||
|
||||
@Nullable
|
||||
private AppRestoredListener appRestoredListener;
|
||||
|
||||
private boolean isActive = false;
|
||||
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to receive callbacks.
|
||||
* @param listener
|
||||
*/
|
||||
public void setStatusChangeListener(@Nullable AppStatusChangeListener listener) {
|
||||
this.statusChangeListener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to receive callbacks.
|
||||
* @param listener
|
||||
*/
|
||||
public void setAppRestoredListener(@Nullable AppRestoredListener listener) {
|
||||
this.appRestoredListener = listener;
|
||||
}
|
||||
|
||||
protected void fireRestoredResult(PluginResult result) {
|
||||
if (appRestoredListener != null) {
|
||||
appRestoredListener.onAppRestored(result);
|
||||
}
|
||||
}
|
||||
|
||||
public void fireStatusChange(boolean isActive) {
|
||||
this.isActive = isActive;
|
||||
if (statusChangeListener != null) {
|
||||
statusChangeListener.onAppStatusChanged(isActive);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class AppUUID {
|
||||
|
||||
private static final String KEY = "CapacitorAppUUID";
|
||||
|
||||
public static String getAppUUID(AppCompatActivity activity) throws Exception {
|
||||
assertAppUUID(activity);
|
||||
return readUUID(activity);
|
||||
}
|
||||
|
||||
public static void regenerateAppUUID(AppCompatActivity activity) throws Exception {
|
||||
try {
|
||||
String uuid = generateUUID();
|
||||
writeUUID(activity, uuid);
|
||||
} catch (NoSuchAlgorithmException ex) {
|
||||
throw new Exception("Capacitor App UUID could not be generated.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void assertAppUUID(AppCompatActivity activity) throws Exception {
|
||||
String uuid = readUUID(activity);
|
||||
if (uuid.equals("")) {
|
||||
regenerateAppUUID(activity);
|
||||
}
|
||||
}
|
||||
|
||||
private static String generateUUID() throws NoSuchAlgorithmException {
|
||||
MessageDigest salt = MessageDigest.getInstance("SHA-256");
|
||||
salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(salt.digest());
|
||||
}
|
||||
|
||||
private static String readUUID(AppCompatActivity activity) {
|
||||
SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE);
|
||||
return sharedPref.getString(KEY, "");
|
||||
}
|
||||
|
||||
private static void writeUUID(AppCompatActivity activity, String uuid) {
|
||||
SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = sharedPref.edit();
|
||||
editor.putString(KEY, uuid);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private static String bytesToHex(byte[] bytes) {
|
||||
byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII);
|
||||
byte[] hexChars = new byte[bytes.length * 2];
|
||||
for (int j = 0; j < bytes.length; j++) {
|
||||
int v = bytes[j] & 0xFF;
|
||||
hexChars[j * 2] = HEX_ARRAY[v >>> 4];
|
||||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
|
||||
}
|
||||
return new String(hexChars, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,212 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.getcapacitor.android.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class BridgeActivity extends AppCompatActivity {
|
||||
|
||||
protected Bridge bridge;
|
||||
protected boolean keepRunning = true;
|
||||
protected CapConfig config;
|
||||
|
||||
protected int activityDepth = 0;
|
||||
protected List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
|
||||
protected final Bridge.Builder bridgeBuilder = new Bridge.Builder(this);
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
bridgeBuilder.setInstanceState(savedInstanceState);
|
||||
getApplication().setTheme(R.style.AppTheme_NoActionBar);
|
||||
setTheme(R.style.AppTheme_NoActionBar);
|
||||
try {
|
||||
setContentView(R.layout.bridge_layout_main);
|
||||
} catch (Exception ex) {
|
||||
setContentView(R.layout.no_webview);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginManager loader = new PluginManager(getAssets());
|
||||
|
||||
try {
|
||||
bridgeBuilder.addPlugins(loader.loadPluginClasses());
|
||||
} catch (PluginLoadException ex) {
|
||||
Logger.error("Error loading plugins.", ex);
|
||||
}
|
||||
|
||||
this.load();
|
||||
}
|
||||
|
||||
protected void load() {
|
||||
Logger.debug("Starting BridgeActivity");
|
||||
|
||||
bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create();
|
||||
|
||||
this.keepRunning = bridge.shouldKeepRunning();
|
||||
this.onNewIntent(getIntent());
|
||||
}
|
||||
|
||||
public void registerPlugin(Class<? extends Plugin> plugin) {
|
||||
bridgeBuilder.addPlugin(plugin);
|
||||
}
|
||||
|
||||
public void registerPlugins(List<Class<? extends Plugin>> plugins) {
|
||||
bridgeBuilder.addPlugins(plugins);
|
||||
}
|
||||
|
||||
public Bridge getBridge() {
|
||||
return this.bridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
bridge.saveInstanceState(outState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
activityDepth++;
|
||||
if (this.bridge != null) {
|
||||
this.bridge.onStart();
|
||||
Logger.debug("App started");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestart() {
|
||||
super.onRestart();
|
||||
this.bridge.onRestart();
|
||||
Logger.debug("App restarted");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (bridge != null) {
|
||||
bridge.getApp().fireStatusChange(true);
|
||||
this.bridge.onResume();
|
||||
Logger.debug("App resumed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
if (bridge != null) {
|
||||
this.bridge.onPause();
|
||||
Logger.debug("App paused");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
if (bridge != null) {
|
||||
activityDepth = Math.max(0, activityDepth - 1);
|
||||
if (activityDepth == 0) {
|
||||
bridge.getApp().fireStatusChange(false);
|
||||
}
|
||||
|
||||
this.bridge.onStop();
|
||||
Logger.debug("App stopped");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.bridge != null) {
|
||||
this.bridge.onDestroy();
|
||||
Logger.debug("App destroyed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
this.bridge.onDetachedFromWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles permission request results.
|
||||
*
|
||||
* Capacitor is backwards compatible such that plugins using legacy permission request codes
|
||||
* may coexist with plugins using the AndroidX Activity v1.2 permission callback flow introduced
|
||||
* in Capacitor 3.0.
|
||||
*
|
||||
* In this method, plugins are checked first for ownership of the legacy permission request code.
|
||||
* If the {@link Bridge#onRequestPermissionsResult(int, String[], int[])} method indicates it has
|
||||
* handled the permission, then the permission callback will be considered complete. Otherwise,
|
||||
* the permission will be handled using the AndroidX Activity flow.
|
||||
*
|
||||
* @param requestCode the request code associated with the permission request
|
||||
* @param permissions the Android permission strings requested
|
||||
* @param grantResults the status result of the permission request
|
||||
*/
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
||||
if (this.bridge == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bridge.onRequestPermissionsResult(requestCode, permissions, grantResults)) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles activity results.
|
||||
*
|
||||
* Capacitor is backwards compatible such that plugins using legacy activity result codes
|
||||
* may coexist with plugins using the AndroidX Activity v1.2 activity callback flow introduced
|
||||
* in Capacitor 3.0.
|
||||
*
|
||||
* In this method, plugins are checked first for ownership of the legacy request code. If the
|
||||
* {@link Bridge#onActivityResult(int, int, Intent)} method indicates it has handled the activity
|
||||
* result, then the callback will be considered complete. Otherwise, the result will be handled
|
||||
* using the AndroidX Activiy flow.
|
||||
*
|
||||
* @param requestCode the request code associated with the activity result
|
||||
* @param resultCode the result code
|
||||
* @param data any data included with the activity result
|
||||
*/
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (this.bridge == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!bridge.onActivityResult(requestCode, resultCode, data)) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
|
||||
if (this.bridge == null || intent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bridge.onNewIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (this.bridge == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.bridge.onConfigurationChanged(newConfig);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.getcapacitor.android.R;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A simple {@link Fragment} subclass.
|
||||
* Use the {@link BridgeFragment#newInstance} factory method to
|
||||
* create an instance of this fragment.
|
||||
*/
|
||||
public class BridgeFragment extends Fragment {
|
||||
|
||||
private static final String ARG_START_DIR = "startDir";
|
||||
|
||||
protected Bridge bridge;
|
||||
protected boolean keepRunning = true;
|
||||
|
||||
private final List<Class<? extends Plugin>> initialPlugins = new ArrayList<>();
|
||||
private CapConfig config = null;
|
||||
|
||||
private final List<WebViewListener> webViewListeners = new ArrayList<>();
|
||||
|
||||
public BridgeFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of
|
||||
* this fragment using the provided parameters.
|
||||
*
|
||||
* @param startDir the directory to serve content from
|
||||
* @return A new instance of fragment BridgeFragment.
|
||||
*/
|
||||
public static BridgeFragment newInstance(String startDir) {
|
||||
BridgeFragment fragment = new BridgeFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ARG_START_DIR, startDir);
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public void addPlugin(Class<? extends Plugin> plugin) {
|
||||
this.initialPlugins.add(plugin);
|
||||
}
|
||||
|
||||
public void setConfig(CapConfig config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public Bridge getBridge() {
|
||||
return bridge;
|
||||
}
|
||||
|
||||
public void addWebViewListener(WebViewListener webViewListener) {
|
||||
webViewListeners.add(webViewListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the WebView and create the Bridge
|
||||
*/
|
||||
protected void load(Bundle savedInstanceState) {
|
||||
Logger.debug("Loading Bridge with BridgeFragment");
|
||||
|
||||
Bundle args = getArguments();
|
||||
String startDir = null;
|
||||
|
||||
if (args != null) {
|
||||
startDir = getArguments().getString(ARG_START_DIR);
|
||||
}
|
||||
|
||||
bridge =
|
||||
new Bridge.Builder(this)
|
||||
.setInstanceState(savedInstanceState)
|
||||
.setPlugins(initialPlugins)
|
||||
.setConfig(config)
|
||||
.addWebViewListeners(webViewListeners)
|
||||
.create();
|
||||
|
||||
if (startDir != null) {
|
||||
bridge.setServerAssetPath(startDir);
|
||||
}
|
||||
|
||||
this.keepRunning = bridge.shouldKeepRunning();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
|
||||
super.onInflate(context, attrs, savedInstanceState);
|
||||
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);
|
||||
CharSequence c = a.getString(R.styleable.bridge_fragment_start_dir);
|
||||
|
||||
if (c != null) {
|
||||
String startDir = c.toString();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ARG_START_DIR, startDir);
|
||||
setArguments(args);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_bridge, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
this.load(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (this.bridge != null) {
|
||||
this.bridge.onDestroy();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,510 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore;
|
||||
import android.view.View;
|
||||
import android.webkit.ConsoleMessage;
|
||||
import android.webkit.GeolocationPermissions;
|
||||
import android.webkit.JsPromptResult;
|
||||
import android.webkit.JsResult;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.PermissionRequest;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebView;
|
||||
import android.widget.EditText;
|
||||
import androidx.activity.result.ActivityResult;
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.core.content.FileProvider;
|
||||
import com.getcapacitor.util.PermissionHelper;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Custom WebChromeClient handler, required for showing dialogs, confirms, etc. in our
|
||||
* WebView instance.
|
||||
*/
|
||||
public class BridgeWebChromeClient extends WebChromeClient {
|
||||
|
||||
private interface PermissionListener {
|
||||
void onPermissionSelect(Boolean isGranted);
|
||||
}
|
||||
|
||||
private interface ActivityResultListener {
|
||||
void onActivityResult(ActivityResult result);
|
||||
}
|
||||
|
||||
private ActivityResultLauncher permissionLauncher;
|
||||
private ActivityResultLauncher activityLauncher;
|
||||
private PermissionListener permissionListener;
|
||||
private ActivityResultListener activityListener;
|
||||
|
||||
private Bridge bridge;
|
||||
|
||||
public BridgeWebChromeClient(Bridge bridge) {
|
||||
this.bridge = bridge;
|
||||
|
||||
ActivityResultCallback<Map<String, Boolean>> permissionCallback = (Map<String, Boolean> isGranted) -> {
|
||||
if (permissionListener != null) {
|
||||
boolean granted = true;
|
||||
for (Map.Entry<String, Boolean> permission : isGranted.entrySet()) {
|
||||
if (!permission.getValue()) granted = false;
|
||||
}
|
||||
permissionListener.onPermissionSelect(granted);
|
||||
}
|
||||
};
|
||||
|
||||
permissionLauncher = bridge.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionCallback);
|
||||
activityLauncher =
|
||||
bridge.registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (activityListener != null) {
|
||||
activityListener.onActivityResult(result);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in `view`.
|
||||
*
|
||||
* Both this method and {@link #onHideCustomView()} are required for
|
||||
* rendering web content in full screen.
|
||||
*
|
||||
* @see <a href="https://developer.android.com/reference/android/webkit/WebChromeClient#onShowCustomView(android.view.View,%20android.webkit.WebChromeClient.CustomViewCallback)">onShowCustomView() docs</a>
|
||||
*/
|
||||
@Override
|
||||
public void onShowCustomView(View view, CustomViewCallback callback) {
|
||||
callback.onCustomViewHidden();
|
||||
super.onShowCustomView(view, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render web content in the original Web View again.
|
||||
*
|
||||
* Do not remove this method--@see #onShowCustomView(View, CustomViewCallback).
|
||||
*/
|
||||
@Override
|
||||
public void onHideCustomView() {
|
||||
super.onHideCustomView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPermissionRequest(final PermissionRequest request) {
|
||||
boolean isRequestPermissionRequired = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M;
|
||||
|
||||
List<String> permissionList = new ArrayList<>();
|
||||
if (Arrays.asList(request.getResources()).contains("android.webkit.resource.VIDEO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.CAMERA);
|
||||
}
|
||||
if (Arrays.asList(request.getResources()).contains("android.webkit.resource.AUDIO_CAPTURE")) {
|
||||
permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS);
|
||||
permissionList.add(Manifest.permission.RECORD_AUDIO);
|
||||
}
|
||||
if (!permissionList.isEmpty() && isRequestPermissionRequired) {
|
||||
String[] permissions = permissionList.toArray(new String[0]);
|
||||
permissionListener =
|
||||
isGranted -> {
|
||||
if (isGranted) {
|
||||
request.grant(request.getResources());
|
||||
} else {
|
||||
request.deny();
|
||||
}
|
||||
};
|
||||
permissionLauncher.launch(permissions);
|
||||
} else {
|
||||
request.grant(request.getResources());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser alert modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
|
||||
if (bridge.getActivity().isFinishing()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK",
|
||||
(dialog, buttonIndex) -> {
|
||||
dialog.dismiss();
|
||||
result.confirm();
|
||||
}
|
||||
)
|
||||
.setOnCancelListener(
|
||||
dialog -> {
|
||||
dialog.dismiss();
|
||||
result.cancel();
|
||||
}
|
||||
);
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser confirm modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) {
|
||||
if (bridge.getActivity().isFinishing()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
|
||||
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setPositiveButton(
|
||||
"OK",
|
||||
(dialog, buttonIndex) -> {
|
||||
dialog.dismiss();
|
||||
result.confirm();
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
"Cancel",
|
||||
(dialog, buttonIndex) -> {
|
||||
dialog.dismiss();
|
||||
result.cancel();
|
||||
}
|
||||
)
|
||||
.setOnCancelListener(
|
||||
dialog -> {
|
||||
dialog.dismiss();
|
||||
result.cancel();
|
||||
}
|
||||
);
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the browser prompt modal
|
||||
* @param view
|
||||
* @param url
|
||||
* @param message
|
||||
* @param defaultValue
|
||||
* @param result
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
|
||||
if (bridge.getActivity().isFinishing()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
|
||||
final EditText input = new EditText(view.getContext());
|
||||
|
||||
builder
|
||||
.setMessage(message)
|
||||
.setView(input)
|
||||
.setPositiveButton(
|
||||
"OK",
|
||||
(dialog, buttonIndex) -> {
|
||||
dialog.dismiss();
|
||||
|
||||
String inputText1 = input.getText().toString().trim();
|
||||
result.confirm(inputText1);
|
||||
}
|
||||
)
|
||||
.setNegativeButton(
|
||||
"Cancel",
|
||||
(dialog, buttonIndex) -> {
|
||||
dialog.dismiss();
|
||||
result.cancel();
|
||||
}
|
||||
)
|
||||
.setOnCancelListener(
|
||||
dialog -> {
|
||||
dialog.dismiss();
|
||||
result.cancel();
|
||||
}
|
||||
);
|
||||
|
||||
AlertDialog dialog = builder.create();
|
||||
|
||||
dialog.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the browser geolocation permission prompt
|
||||
* @param origin
|
||||
* @param callback
|
||||
*/
|
||||
@Override
|
||||
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
|
||||
super.onGeolocationPermissionsShowPrompt(origin, callback);
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin);
|
||||
final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION };
|
||||
|
||||
if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) {
|
||||
permissionListener =
|
||||
isGranted -> {
|
||||
if (isGranted) {
|
||||
callback.invoke(origin, true, false);
|
||||
} else {
|
||||
final String[] coarsePermission = { Manifest.permission.ACCESS_COARSE_LOCATION };
|
||||
if (
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||
PermissionHelper.hasPermissions(bridge.getContext(), coarsePermission)
|
||||
) {
|
||||
callback.invoke(origin, true, false);
|
||||
} else {
|
||||
callback.invoke(origin, false, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
permissionLauncher.launch(geoPermissions);
|
||||
} else {
|
||||
// permission is already granted
|
||||
callback.invoke(origin, true, false);
|
||||
Logger.debug("onGeolocationPermissionsShowPrompt: has required permission");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onShowFileChooser(
|
||||
WebView webView,
|
||||
final ValueCallback<Uri[]> filePathCallback,
|
||||
final FileChooserParams fileChooserParams
|
||||
) {
|
||||
List<String> acceptTypes = Arrays.asList(fileChooserParams.getAcceptTypes());
|
||||
boolean captureEnabled = fileChooserParams.isCaptureEnabled();
|
||||
boolean capturePhoto = captureEnabled && acceptTypes.contains("image/*");
|
||||
final boolean captureVideo = captureEnabled && acceptTypes.contains("video/*");
|
||||
if ((capturePhoto || captureVideo)) {
|
||||
if (isMediaCaptureSupported()) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo);
|
||||
} else {
|
||||
permissionListener =
|
||||
isGranted -> {
|
||||
if (isGranted) {
|
||||
showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo);
|
||||
} else {
|
||||
Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted");
|
||||
filePathCallback.onReceiveValue(null);
|
||||
}
|
||||
};
|
||||
final String[] camPermission = { Manifest.permission.CAMERA };
|
||||
permissionLauncher.launch(camPermission);
|
||||
}
|
||||
} else {
|
||||
showFilePicker(filePathCallback, fileChooserParams);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isMediaCaptureSupported() {
|
||||
String[] permissions = { Manifest.permission.CAMERA };
|
||||
return (
|
||||
PermissionHelper.hasPermissions(bridge.getContext(), permissions) ||
|
||||
!PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA)
|
||||
);
|
||||
}
|
||||
|
||||
private void showMediaCaptureOrFilePicker(ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams, boolean isVideo) {
|
||||
// TODO: add support for video capture on Android M and older
|
||||
// On Android M and lower the VIDEO_CAPTURE_INTENT (e.g.: intent.getData())
|
||||
// returns a file:// URI instead of the expected content:// URI.
|
||||
// So we disable it for now because it requires a bit more work
|
||||
boolean isVideoCaptureSupported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N;
|
||||
boolean shown = false;
|
||||
if (isVideo && isVideoCaptureSupported) {
|
||||
shown = showVideoCapturePicker(filePathCallback);
|
||||
} else {
|
||||
shown = showImageCapturePicker(filePathCallback);
|
||||
}
|
||||
if (!shown) {
|
||||
Logger.warn(Logger.tags("FileChooser"), "Media capture intent could not be launched. Falling back to default file picker.");
|
||||
showFilePicker(filePathCallback, fileChooserParams);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
private boolean showImageCapturePicker(final ValueCallback<Uri[]> filePathCallback) {
|
||||
Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (takePictureIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final Uri imageFileUri;
|
||||
try {
|
||||
imageFileUri = createImageFileUri();
|
||||
} catch (Exception ex) {
|
||||
Logger.error("Unable to create temporary media capture file: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri);
|
||||
activityListener =
|
||||
activityResult -> {
|
||||
Uri[] result = null;
|
||||
if (activityResult.getResultCode() == Activity.RESULT_OK) {
|
||||
result = new Uri[] { imageFileUri };
|
||||
}
|
||||
filePathCallback.onReceiveValue(result);
|
||||
};
|
||||
activityLauncher.launch(takePictureIntent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@SuppressLint("QueryPermissionsNeeded")
|
||||
private boolean showVideoCapturePicker(final ValueCallback<Uri[]> filePathCallback) {
|
||||
Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
|
||||
if (takeVideoIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
activityListener =
|
||||
activityResult -> {
|
||||
Uri[] result = null;
|
||||
if (activityResult.getResultCode() == Activity.RESULT_OK) {
|
||||
result = new Uri[] { activityResult.getData().getData() };
|
||||
}
|
||||
filePathCallback.onReceiveValue(result);
|
||||
};
|
||||
activityLauncher.launch(takeVideoIntent);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showFilePicker(final ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
|
||||
Intent intent = fileChooserParams.createIntent();
|
||||
if (fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE) {
|
||||
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||
}
|
||||
if (fileChooserParams.getAcceptTypes().length > 1 || intent.getType().startsWith(".")) {
|
||||
String[] validTypes = getValidTypes(fileChooserParams.getAcceptTypes());
|
||||
intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes);
|
||||
if (intent.getType().startsWith(".")) {
|
||||
intent.setType(validTypes[0]);
|
||||
}
|
||||
}
|
||||
try {
|
||||
activityListener =
|
||||
activityResult -> {
|
||||
Uri[] result;
|
||||
Intent resultIntent = activityResult.getData();
|
||||
if (activityResult.getResultCode() == Activity.RESULT_OK && resultIntent.getClipData() != null) {
|
||||
final int numFiles = resultIntent.getClipData().getItemCount();
|
||||
result = new Uri[numFiles];
|
||||
for (int i = 0; i < numFiles; i++) {
|
||||
result[i] = resultIntent.getClipData().getItemAt(i).getUri();
|
||||
}
|
||||
} else {
|
||||
result = WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), resultIntent);
|
||||
}
|
||||
filePathCallback.onReceiveValue(result);
|
||||
};
|
||||
activityLauncher.launch(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
filePathCallback.onReceiveValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String[] getValidTypes(String[] currentTypes) {
|
||||
List<String> validTypes = new ArrayList<>();
|
||||
MimeTypeMap mtm = MimeTypeMap.getSingleton();
|
||||
for (String mime : currentTypes) {
|
||||
if (mime.startsWith(".")) {
|
||||
String extension = mime.substring(1);
|
||||
String extensionMime = mtm.getMimeTypeFromExtension(extension);
|
||||
if (extensionMime != null && !validTypes.contains(extensionMime)) {
|
||||
validTypes.add(extensionMime);
|
||||
}
|
||||
} else if (!validTypes.contains(mime)) {
|
||||
validTypes.add(mime);
|
||||
}
|
||||
}
|
||||
Object[] validObj = validTypes.toArray();
|
||||
return Arrays.copyOf(validObj, validObj.length, String[].class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||
String tag = Logger.tags("Console");
|
||||
if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) {
|
||||
String msg = String.format(
|
||||
"File: %s - Line %d - Msg: %s",
|
||||
consoleMessage.sourceId(),
|
||||
consoleMessage.lineNumber(),
|
||||
consoleMessage.message()
|
||||
);
|
||||
String level = consoleMessage.messageLevel().name();
|
||||
if ("ERROR".equalsIgnoreCase(level)) {
|
||||
Logger.error(tag, msg, null);
|
||||
} else if ("WARNING".equalsIgnoreCase(level)) {
|
||||
Logger.warn(tag, msg);
|
||||
} else if ("TIP".equalsIgnoreCase(level)) {
|
||||
Logger.debug(tag, msg);
|
||||
} else {
|
||||
Logger.info(tag, msg);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isValidMsg(String msg) {
|
||||
return !(
|
||||
msg.contains("%cresult %c") ||
|
||||
(msg.contains("%cnative %c")) ||
|
||||
msg.equalsIgnoreCase("[object Object]") ||
|
||||
msg.equalsIgnoreCase("console.groupEnd")
|
||||
);
|
||||
}
|
||||
|
||||
private Uri createImageFileUri() throws IOException {
|
||||
Activity activity = bridge.getActivity();
|
||||
File photoFile = createImageFile(activity);
|
||||
return FileProvider.getUriForFile(activity, bridge.getContext().getPackageName() + ".fileprovider", photoFile);
|
||||
}
|
||||
|
||||
private File createImageFile(Activity activity) throws IOException {
|
||||
// Create an image file name
|
||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
|
||||
String imageFileName = "JPEG_" + timeStamp + "_";
|
||||
File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||
|
||||
return File.createTempFile(imageFileName, ".jpg", storageDir);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.webkit.RenderProcessGoneDetail;
|
||||
import android.webkit.WebResourceError;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import java.util.List;
|
||||
|
||||
public class BridgeWebViewClient extends WebViewClient {
|
||||
|
||||
private Bridge bridge;
|
||||
|
||||
public BridgeWebViewClient(Bridge bridge) {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
|
||||
return bridge.getLocalServer().shouldInterceptRequest(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
|
||||
Uri url = request.getUrl();
|
||||
return bridge.launchIntent(url);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean shouldOverrideUrlLoading(WebView view, String url) {
|
||||
return bridge.launchIntent(Uri.parse(url));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageFinished(WebView view, String url) {
|
||||
super.onPageFinished(view, url);
|
||||
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
|
||||
|
||||
if (webViewListeners != null && view.getProgress() == 100) {
|
||||
for (WebViewListener listener : bridge.getWebViewListeners()) {
|
||||
listener.onPageLoaded(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) {
|
||||
super.onReceivedError(view, request, error);
|
||||
|
||||
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
|
||||
if (webViewListeners != null) {
|
||||
for (WebViewListener listener : bridge.getWebViewListeners()) {
|
||||
listener.onReceivedError(view);
|
||||
}
|
||||
}
|
||||
|
||||
String errorPath = bridge.getErrorUrl();
|
||||
if (errorPath != null && request.isForMainFrame()) {
|
||||
view.loadUrl(errorPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageStarted(WebView view, String url, Bitmap favicon) {
|
||||
super.onPageStarted(view, url, favicon);
|
||||
bridge.reset();
|
||||
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
|
||||
|
||||
if (webViewListeners != null) {
|
||||
for (WebViewListener listener : bridge.getWebViewListeners()) {
|
||||
listener.onPageStarted(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) {
|
||||
super.onReceivedHttpError(view, request, errorResponse);
|
||||
|
||||
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
|
||||
if (webViewListeners != null) {
|
||||
for (WebViewListener listener : bridge.getWebViewListeners()) {
|
||||
listener.onReceivedHttpError(view);
|
||||
}
|
||||
}
|
||||
|
||||
String errorPath = bridge.getErrorUrl();
|
||||
if (errorPath != null && request.isForMainFrame()) {
|
||||
view.loadUrl(errorPath);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) {
|
||||
super.onRenderProcessGone(view, detail);
|
||||
boolean result = false;
|
||||
|
||||
List<WebViewListener> webViewListeners = bridge.getWebViewListeners();
|
||||
if (webViewListeners != null) {
|
||||
for (WebViewListener listener : bridge.getWebViewListeners()) {
|
||||
result = listener.onRenderProcessGone(view, detail) || result;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,683 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import static com.getcapacitor.Bridge.CAPACITOR_HTTPS_SCHEME;
|
||||
import static com.getcapacitor.Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION;
|
||||
import static com.getcapacitor.Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION;
|
||||
import static com.getcapacitor.Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION;
|
||||
import static com.getcapacitor.Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION;
|
||||
import static com.getcapacitor.FileUtils.readFileFromAssets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ApplicationInfo;
|
||||
import android.content.res.AssetManager;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.getcapacitor.util.JSONUtils;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents the configuration options for Capacitor
|
||||
*/
|
||||
public class CapConfig {
|
||||
|
||||
private static final String LOG_BEHAVIOR_NONE = "none";
|
||||
private static final String LOG_BEHAVIOR_DEBUG = "debug";
|
||||
private static final String LOG_BEHAVIOR_PRODUCTION = "production";
|
||||
|
||||
// Server Config
|
||||
private boolean html5mode = true;
|
||||
private String serverUrl;
|
||||
private String hostname = "localhost";
|
||||
private String androidScheme = CAPACITOR_HTTPS_SCHEME;
|
||||
private String[] allowNavigation;
|
||||
|
||||
// Android Config
|
||||
private String overriddenUserAgentString;
|
||||
private String appendedUserAgentString;
|
||||
private String backgroundColor;
|
||||
private boolean allowMixedContent = false;
|
||||
private boolean captureInput = false;
|
||||
private boolean webContentsDebuggingEnabled = false;
|
||||
private boolean loggingEnabled = true;
|
||||
private boolean initialFocus = true;
|
||||
private boolean useLegacyBridge = false;
|
||||
private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION;
|
||||
private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION;
|
||||
private String errorPath;
|
||||
private boolean zoomableWebView = false;
|
||||
|
||||
// Embedded
|
||||
private String startPath;
|
||||
|
||||
// Plugins
|
||||
private Map<String, PluginConfig> pluginsConfiguration = null;
|
||||
|
||||
// Config Object JSON (legacy)
|
||||
private JSONObject configJSON = new JSONObject();
|
||||
|
||||
/**
|
||||
* Constructs an empty config file.
|
||||
*/
|
||||
private CapConfig() {}
|
||||
|
||||
/**
|
||||
* Get an instance of the Config file object.
|
||||
* @deprecated use {@link #loadDefault(Context)} to load an instance of the Config object
|
||||
* from the capacitor.config.json file, or use the {@link CapConfig.Builder} to construct
|
||||
* a CapConfig for embedded use.
|
||||
*
|
||||
* @param assetManager The AssetManager used to load the config file
|
||||
* @param config JSON describing a configuration to use
|
||||
*/
|
||||
@Deprecated
|
||||
public CapConfig(AssetManager assetManager, JSONObject config) {
|
||||
if (config != null) {
|
||||
this.configJSON = config;
|
||||
} else {
|
||||
// Load the capacitor.config.json
|
||||
loadConfigFromAssets(assetManager, null);
|
||||
}
|
||||
|
||||
deserializeConfig(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Capacitor Configuration from config.json file.
|
||||
*
|
||||
* @param context The context.
|
||||
* @return A loaded config file, if successful.
|
||||
*/
|
||||
public static CapConfig loadDefault(Context context) {
|
||||
CapConfig config = new CapConfig();
|
||||
|
||||
if (context == null) {
|
||||
Logger.error("Capacitor Config could not be created from file. Context must not be null.");
|
||||
return config;
|
||||
}
|
||||
|
||||
config.loadConfigFromAssets(context.getAssets(), null);
|
||||
config.deserializeConfig(context);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Capacitor Configuration from config.json file within the app assets.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param path A path relative to the root assets directory.
|
||||
* @return A loaded config file, if successful.
|
||||
*/
|
||||
public static CapConfig loadFromAssets(Context context, String path) {
|
||||
CapConfig config = new CapConfig();
|
||||
|
||||
if (context == null) {
|
||||
Logger.error("Capacitor Config could not be created from file. Context must not be null.");
|
||||
return config;
|
||||
}
|
||||
|
||||
config.loadConfigFromAssets(context.getAssets(), path);
|
||||
config.deserializeConfig(context);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Capacitor Configuration from config.json file within the app file-space.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param path A path relative to the root of the app file-space.
|
||||
* @return A loaded config file, if successful.
|
||||
*/
|
||||
public static CapConfig loadFromFile(Context context, String path) {
|
||||
CapConfig config = new CapConfig();
|
||||
|
||||
if (context == null) {
|
||||
Logger.error("Capacitor Config could not be created from file. Context must not be null.");
|
||||
return config;
|
||||
}
|
||||
|
||||
config.loadConfigFromFile(path);
|
||||
config.deserializeConfig(context);
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a Capacitor Configuration using ConfigBuilder.
|
||||
*
|
||||
* @param builder A config builder initialized with values
|
||||
*/
|
||||
private CapConfig(Builder builder) {
|
||||
// Server Config
|
||||
this.html5mode = builder.html5mode;
|
||||
this.serverUrl = builder.serverUrl;
|
||||
this.hostname = builder.hostname;
|
||||
|
||||
if (this.validateScheme(builder.androidScheme)) {
|
||||
this.androidScheme = builder.androidScheme;
|
||||
}
|
||||
|
||||
this.allowNavigation = builder.allowNavigation;
|
||||
|
||||
// Android Config
|
||||
this.overriddenUserAgentString = builder.overriddenUserAgentString;
|
||||
this.appendedUserAgentString = builder.appendedUserAgentString;
|
||||
this.backgroundColor = builder.backgroundColor;
|
||||
this.allowMixedContent = builder.allowMixedContent;
|
||||
this.captureInput = builder.captureInput;
|
||||
this.webContentsDebuggingEnabled = builder.webContentsDebuggingEnabled;
|
||||
this.loggingEnabled = builder.loggingEnabled;
|
||||
this.initialFocus = builder.initialFocus;
|
||||
this.useLegacyBridge = builder.useLegacyBridge;
|
||||
this.minWebViewVersion = builder.minWebViewVersion;
|
||||
this.minHuaweiWebViewVersion = builder.minHuaweiWebViewVersion;
|
||||
this.errorPath = builder.errorPath;
|
||||
this.zoomableWebView = builder.zoomableWebView;
|
||||
|
||||
// Embedded
|
||||
this.startPath = builder.startPath;
|
||||
|
||||
// Plugins Config
|
||||
this.pluginsConfiguration = builder.pluginsConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Capacitor Configuration JSON file into a Capacitor Configuration object.
|
||||
* An optional path string can be provided to look for the config in a subdirectory path.
|
||||
*/
|
||||
private void loadConfigFromAssets(AssetManager assetManager, String path) {
|
||||
if (path == null) {
|
||||
path = "";
|
||||
} else {
|
||||
// Add slash at the end to form a proper file path if going deeper in assets dir
|
||||
if (path.charAt(path.length() - 1) != '/') {
|
||||
path = path + "/";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
String jsonString = readFileFromAssets(assetManager, path + "capacitor.config.json");
|
||||
configJSON = new JSONObject(jsonString);
|
||||
} catch (IOException ex) {
|
||||
Logger.error("Unable to load capacitor.config.json. Run npx cap copy first", ex);
|
||||
} catch (JSONException ex) {
|
||||
Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Capacitor Configuration JSON file into a Capacitor Configuration object.
|
||||
* An optional path string can be provided to look for the config in a subdirectory path.
|
||||
*/
|
||||
private void loadConfigFromFile(String path) {
|
||||
if (path == null) {
|
||||
path = "";
|
||||
} else {
|
||||
// Add slash at the end to form a proper file path if going deeper in assets dir
|
||||
if (path.charAt(path.length() - 1) != '/') {
|
||||
path = path + "/";
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
File configFile = new File(path + "capacitor.config.json");
|
||||
String jsonString = FileUtils.readFileFromDisk(configFile);
|
||||
configJSON = new JSONObject(jsonString);
|
||||
} catch (JSONException ex) {
|
||||
Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex);
|
||||
} catch (IOException ex) {
|
||||
Logger.error("Unable to load capacitor.config.json.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserializes the config from JSON into a Capacitor Configuration object.
|
||||
*/
|
||||
private void deserializeConfig(@Nullable Context context) {
|
||||
boolean isDebug = context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
||||
|
||||
// Server
|
||||
html5mode = JSONUtils.getBoolean(configJSON, "server.html5mode", html5mode);
|
||||
serverUrl = JSONUtils.getString(configJSON, "server.url", null);
|
||||
hostname = JSONUtils.getString(configJSON, "server.hostname", hostname);
|
||||
errorPath = JSONUtils.getString(configJSON, "server.errorPath", null);
|
||||
|
||||
String configSchema = JSONUtils.getString(configJSON, "server.androidScheme", androidScheme);
|
||||
if (this.validateScheme(configSchema)) {
|
||||
androidScheme = configSchema;
|
||||
}
|
||||
|
||||
allowNavigation = JSONUtils.getArray(configJSON, "server.allowNavigation", null);
|
||||
|
||||
// Android
|
||||
overriddenUserAgentString =
|
||||
JSONUtils.getString(configJSON, "android.overrideUserAgent", JSONUtils.getString(configJSON, "overrideUserAgent", null));
|
||||
appendedUserAgentString =
|
||||
JSONUtils.getString(configJSON, "android.appendUserAgent", JSONUtils.getString(configJSON, "appendUserAgent", null));
|
||||
backgroundColor =
|
||||
JSONUtils.getString(configJSON, "android.backgroundColor", JSONUtils.getString(configJSON, "backgroundColor", null));
|
||||
allowMixedContent =
|
||||
JSONUtils.getBoolean(
|
||||
configJSON,
|
||||
"android.allowMixedContent",
|
||||
JSONUtils.getBoolean(configJSON, "allowMixedContent", allowMixedContent)
|
||||
);
|
||||
minWebViewVersion = JSONUtils.getInt(configJSON, "android.minWebViewVersion", DEFAULT_ANDROID_WEBVIEW_VERSION);
|
||||
minHuaweiWebViewVersion = JSONUtils.getInt(configJSON, "android.minHuaweiWebViewVersion", DEFAULT_HUAWEI_WEBVIEW_VERSION);
|
||||
captureInput = JSONUtils.getBoolean(configJSON, "android.captureInput", captureInput);
|
||||
useLegacyBridge = JSONUtils.getBoolean(configJSON, "android.useLegacyBridge", useLegacyBridge);
|
||||
webContentsDebuggingEnabled = JSONUtils.getBoolean(configJSON, "android.webContentsDebuggingEnabled", isDebug);
|
||||
zoomableWebView = JSONUtils.getBoolean(configJSON, "android.zoomEnabled", JSONUtils.getBoolean(configJSON, "zoomEnabled", false));
|
||||
|
||||
String logBehavior = JSONUtils.getString(
|
||||
configJSON,
|
||||
"android.loggingBehavior",
|
||||
JSONUtils.getString(configJSON, "loggingBehavior", LOG_BEHAVIOR_DEBUG)
|
||||
);
|
||||
switch (logBehavior.toLowerCase(Locale.ROOT)) {
|
||||
case LOG_BEHAVIOR_PRODUCTION:
|
||||
loggingEnabled = true;
|
||||
break;
|
||||
case LOG_BEHAVIOR_NONE:
|
||||
loggingEnabled = false;
|
||||
break;
|
||||
default: // LOG_BEHAVIOR_DEBUG
|
||||
loggingEnabled = isDebug;
|
||||
}
|
||||
|
||||
initialFocus = JSONUtils.getBoolean(configJSON, "android.initialFocus", initialFocus);
|
||||
|
||||
// Plugins
|
||||
pluginsConfiguration = deserializePluginsConfig(JSONUtils.getObject(configJSON, "plugins"));
|
||||
}
|
||||
|
||||
private boolean validateScheme(String scheme) {
|
||||
List<String> invalidSchemes = Arrays.asList("file", "ftp", "ftps", "ws", "wss", "about", "blob", "data");
|
||||
if (invalidSchemes.contains(scheme)) {
|
||||
Logger.warn(scheme + " is not an allowed scheme. Defaulting to https.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-http(s) schemes are not allowed to modify the URL path as of Android Webview 117
|
||||
if (!scheme.equals("http") && !scheme.equals("https")) {
|
||||
Logger.warn(
|
||||
"Using a non-standard scheme: " + scheme + " for Android. This is known to cause issues as of Android Webview 117."
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isHTML5Mode() {
|
||||
return html5mode;
|
||||
}
|
||||
|
||||
public String getServerUrl() {
|
||||
return serverUrl;
|
||||
}
|
||||
|
||||
public String getErrorPath() {
|
||||
return errorPath;
|
||||
}
|
||||
|
||||
public String getHostname() {
|
||||
return hostname;
|
||||
}
|
||||
|
||||
public String getStartPath() {
|
||||
return startPath;
|
||||
}
|
||||
|
||||
public String getAndroidScheme() {
|
||||
return androidScheme;
|
||||
}
|
||||
|
||||
public String[] getAllowNavigation() {
|
||||
return allowNavigation;
|
||||
}
|
||||
|
||||
public String getOverriddenUserAgentString() {
|
||||
return overriddenUserAgentString;
|
||||
}
|
||||
|
||||
public String getAppendedUserAgentString() {
|
||||
return appendedUserAgentString;
|
||||
}
|
||||
|
||||
public String getBackgroundColor() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
public boolean isMixedContentAllowed() {
|
||||
return allowMixedContent;
|
||||
}
|
||||
|
||||
public boolean isInputCaptured() {
|
||||
return captureInput;
|
||||
}
|
||||
|
||||
public boolean isWebContentsDebuggingEnabled() {
|
||||
return webContentsDebuggingEnabled;
|
||||
}
|
||||
|
||||
public boolean isZoomableWebView() {
|
||||
return zoomableWebView;
|
||||
}
|
||||
|
||||
public boolean isLoggingEnabled() {
|
||||
return loggingEnabled;
|
||||
}
|
||||
|
||||
public boolean isInitialFocus() {
|
||||
return initialFocus;
|
||||
}
|
||||
|
||||
public boolean isUsingLegacyBridge() {
|
||||
return useLegacyBridge;
|
||||
}
|
||||
|
||||
public int getMinWebViewVersion() {
|
||||
if (minWebViewVersion < MINIMUM_ANDROID_WEBVIEW_VERSION) {
|
||||
Logger.warn("Specified minimum webview version is too low, defaulting to " + MINIMUM_ANDROID_WEBVIEW_VERSION);
|
||||
return MINIMUM_ANDROID_WEBVIEW_VERSION;
|
||||
}
|
||||
|
||||
return minWebViewVersion;
|
||||
}
|
||||
|
||||
public int getMinHuaweiWebViewVersion() {
|
||||
if (minHuaweiWebViewVersion < MINIMUM_HUAWEI_WEBVIEW_VERSION) {
|
||||
Logger.warn("Specified minimum Huawei webview version is too low, defaulting to " + MINIMUM_HUAWEI_WEBVIEW_VERSION);
|
||||
return MINIMUM_HUAWEI_WEBVIEW_VERSION;
|
||||
}
|
||||
|
||||
return minHuaweiWebViewVersion;
|
||||
}
|
||||
|
||||
public PluginConfig getPluginConfiguration(String pluginId) {
|
||||
PluginConfig pluginConfig = pluginsConfiguration.get(pluginId);
|
||||
if (pluginConfig == null) {
|
||||
pluginConfig = new PluginConfig(new JSONObject());
|
||||
}
|
||||
|
||||
return pluginConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON object value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getObject(String)} to access plugin config values.
|
||||
* For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
@Deprecated
|
||||
public JSONObject getObject(String key) {
|
||||
try {
|
||||
return configJSON.getJSONObject(key);
|
||||
} catch (Exception ex) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config
|
||||
* values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
@Deprecated
|
||||
public String getString(String key) {
|
||||
return JSONUtils.getString(configJSON, key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config
|
||||
* values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
@Deprecated
|
||||
public String getString(String key, String defaultValue) {
|
||||
return JSONUtils.getString(configJSON, key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a boolean value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getBoolean(String, boolean)} to access plugin config
|
||||
* values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean getBoolean(String key, boolean defaultValue) {
|
||||
return JSONUtils.getBoolean(configJSON, key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an integer value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getInt(String, int)} to access the plugin config
|
||||
* values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
@Deprecated
|
||||
public int getInt(String key, int defaultValue) {
|
||||
return JSONUtils.getInt(configJSON, key, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string array value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getArray(String)} to access the plugin config
|
||||
* values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
@Deprecated
|
||||
public String[] getArray(String key) {
|
||||
return JSONUtils.getArray(configJSON, key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string array value from the Capacitor config.
|
||||
* @deprecated use {@link PluginConfig#getArray(String, String[])} to access the plugin
|
||||
* config values. For main Capacitor config values, use the appropriate getter.
|
||||
*
|
||||
* @param key A key to fetch from the config
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
@Deprecated
|
||||
public String[] getArray(String key, String[] defaultValue) {
|
||||
return JSONUtils.getArray(configJSON, key, defaultValue);
|
||||
}
|
||||
|
||||
private static Map<String, PluginConfig> deserializePluginsConfig(JSONObject pluginsConfig) {
|
||||
Map<String, PluginConfig> pluginsMap = new HashMap<>();
|
||||
|
||||
// return an empty map if there is no pluginsConfig json
|
||||
if (pluginsConfig == null) {
|
||||
return pluginsMap;
|
||||
}
|
||||
|
||||
Iterator<String> pluginIds = pluginsConfig.keys();
|
||||
|
||||
while (pluginIds.hasNext()) {
|
||||
String pluginId = pluginIds.next();
|
||||
JSONObject value = null;
|
||||
|
||||
try {
|
||||
value = pluginsConfig.getJSONObject(pluginId);
|
||||
PluginConfig pluginConfig = new PluginConfig(value);
|
||||
pluginsMap.put(pluginId, pluginConfig);
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return pluginsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Capacitor Configuration in code
|
||||
*/
|
||||
public static class Builder {
|
||||
|
||||
private Context context;
|
||||
|
||||
// Server Config Values
|
||||
private boolean html5mode = true;
|
||||
private String serverUrl;
|
||||
private String errorPath;
|
||||
private String hostname = "localhost";
|
||||
private String androidScheme = CAPACITOR_HTTPS_SCHEME;
|
||||
private String[] allowNavigation;
|
||||
|
||||
// Android Config Values
|
||||
private String overriddenUserAgentString;
|
||||
private String appendedUserAgentString;
|
||||
private String backgroundColor;
|
||||
private boolean allowMixedContent = false;
|
||||
private boolean captureInput = false;
|
||||
private Boolean webContentsDebuggingEnabled = null;
|
||||
private boolean loggingEnabled = true;
|
||||
private boolean initialFocus = false;
|
||||
private boolean useLegacyBridge = false;
|
||||
private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION;
|
||||
private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION;
|
||||
private boolean zoomableWebView = false;
|
||||
|
||||
// Embedded
|
||||
private String startPath = null;
|
||||
|
||||
// Plugins Config Object
|
||||
private Map<String, PluginConfig> pluginsConfiguration = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Constructs a new CapConfig Builder.
|
||||
*
|
||||
* @param context The context
|
||||
*/
|
||||
public Builder(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Capacitor Config from the builder.
|
||||
*
|
||||
* @return A new Capacitor Config
|
||||
*/
|
||||
public CapConfig create() {
|
||||
if (webContentsDebuggingEnabled == null) {
|
||||
webContentsDebuggingEnabled = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
|
||||
}
|
||||
|
||||
return new CapConfig(this);
|
||||
}
|
||||
|
||||
public Builder setPluginsConfiguration(JSONObject pluginsConfiguration) {
|
||||
this.pluginsConfiguration = deserializePluginsConfig(pluginsConfiguration);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHTML5mode(boolean html5mode) {
|
||||
this.html5mode = html5mode;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setServerUrl(String serverUrl) {
|
||||
this.serverUrl = serverUrl;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setErrorPath(String errorPath) {
|
||||
this.errorPath = errorPath;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setHostname(String hostname) {
|
||||
this.hostname = hostname;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setStartPath(String path) {
|
||||
this.startPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAndroidScheme(String androidScheme) {
|
||||
this.androidScheme = androidScheme;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowNavigation(String[] allowNavigation) {
|
||||
this.allowNavigation = allowNavigation;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setOverriddenUserAgentString(String overriddenUserAgentString) {
|
||||
this.overriddenUserAgentString = overriddenUserAgentString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAppendedUserAgentString(String appendedUserAgentString) {
|
||||
this.appendedUserAgentString = appendedUserAgentString;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBackgroundColor(String backgroundColor) {
|
||||
this.backgroundColor = backgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setAllowMixedContent(boolean allowMixedContent) {
|
||||
this.allowMixedContent = allowMixedContent;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setCaptureInput(boolean captureInput) {
|
||||
this.captureInput = captureInput;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setUseLegacyBridge(boolean useLegacyBridge) {
|
||||
this.useLegacyBridge = useLegacyBridge;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setWebContentsDebuggingEnabled(boolean webContentsDebuggingEnabled) {
|
||||
this.webContentsDebuggingEnabled = webContentsDebuggingEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setZoomableWebView(boolean zoomableWebView) {
|
||||
this.zoomableWebView = zoomableWebView;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setLoggingEnabled(boolean enabled) {
|
||||
this.loggingEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setInitialFocus(boolean focus) {
|
||||
this.initialFocus = focus;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.webkit.WebView;
|
||||
|
||||
public class CapacitorWebView extends WebView {
|
||||
|
||||
private BaseInputConnection capInputConnection;
|
||||
private Bridge bridge;
|
||||
|
||||
public CapacitorWebView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void setBridge(Bridge bridge) {
|
||||
this.bridge = bridge;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
CapConfig config;
|
||||
if (bridge != null) {
|
||||
config = bridge.getConfig();
|
||||
} else {
|
||||
config = CapConfig.loadDefault(getContext());
|
||||
}
|
||||
|
||||
boolean captureInput = config.isInputCaptured();
|
||||
if (captureInput) {
|
||||
if (capInputConnection == null) {
|
||||
capInputConnection = new BaseInputConnection(this, false);
|
||||
}
|
||||
return capInputConnection;
|
||||
}
|
||||
return super.onCreateInputConnection(outAttrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_MULTIPLE) {
|
||||
evaluateJavascript("document.activeElement.value = document.activeElement.value + '" + event.getCharacters() + "';", null);
|
||||
return false;
|
||||
}
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,292 @@
|
|||
/**
|
||||
* Portions adopted from react-native-image-crop-picker
|
||||
*
|
||||
* MIT License
|
||||
|
||||
* Copyright (c) 2017 Ivan Pusic
|
||||
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
package com.getcapacitor;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
/**
|
||||
* Common File utilities, such as resolve content URIs and
|
||||
* creating portable web paths from low-level files
|
||||
*/
|
||||
public class FileUtils {
|
||||
|
||||
private static String CapacitorFileScheme = Bridge.CAPACITOR_FILE_START;
|
||||
|
||||
public enum Type {
|
||||
IMAGE("image");
|
||||
|
||||
private String type;
|
||||
|
||||
Type(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
|
||||
public static String getPortablePath(Context c, String host, Uri u) {
|
||||
String path = getFileUrlForUri(c, u);
|
||||
if (path.startsWith("file://")) {
|
||||
path = path.replace("file://", "");
|
||||
}
|
||||
return host + Bridge.CAPACITOR_FILE_START + path;
|
||||
}
|
||||
|
||||
public static String getFileUrlForUri(final Context context, final Uri uri) {
|
||||
// DocumentProvider
|
||||
if (DocumentsContract.isDocumentUri(context, uri)) {
|
||||
// ExternalStorageProvider
|
||||
if (isExternalStorageDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
if ("primary".equalsIgnoreCase(type)) {
|
||||
return legacyPrimaryPath(split[1]);
|
||||
} else {
|
||||
final int splitIndex = docId.indexOf(':', 1);
|
||||
final String tag = docId.substring(0, splitIndex);
|
||||
final String path = docId.substring(splitIndex + 1);
|
||||
|
||||
String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag);
|
||||
if (nonPrimaryVolume != null) {
|
||||
String result = nonPrimaryVolume + "/" + path;
|
||||
File file = new File(result);
|
||||
if (file.exists() && file.canRead()) {
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
// DownloadsProvider
|
||||
else if (isDownloadsDocument(uri)) {
|
||||
final String id = DocumentsContract.getDocumentId(uri);
|
||||
final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
|
||||
|
||||
return getDataColumn(context, contentUri, null, null);
|
||||
}
|
||||
// MediaProvider
|
||||
else if (isMediaDocument(uri)) {
|
||||
final String docId = DocumentsContract.getDocumentId(uri);
|
||||
final String[] split = docId.split(":");
|
||||
final String type = split[0];
|
||||
|
||||
Uri contentUri = null;
|
||||
if ("image".equals(type)) {
|
||||
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("video".equals(type)) {
|
||||
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
|
||||
} else if ("audio".equals(type)) {
|
||||
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
|
||||
}
|
||||
|
||||
final String selection = "_id=?";
|
||||
final String[] selectionArgs = new String[] { split[1] };
|
||||
|
||||
return getDataColumn(context, contentUri, selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
// MediaStore (and general)
|
||||
else if ("content".equalsIgnoreCase(uri.getScheme())) {
|
||||
// Return the remote address
|
||||
if (isGooglePhotosUri(uri)) return uri.getLastPathSegment();
|
||||
return getDataColumn(context, uri, null, null);
|
||||
}
|
||||
// File
|
||||
else if ("file".equalsIgnoreCase(uri.getScheme())) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static String legacyPrimaryPath(String pathPart) {
|
||||
return Environment.getExternalStorageDirectory() + "/" + pathPart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a plaintext file from the assets directory.
|
||||
*
|
||||
* @param assetManager Used to open the file.
|
||||
* @param fileName The path of the file to read.
|
||||
* @return The contents of the file path.
|
||||
* @throws IOException Thrown if any issues reading the provided file path.
|
||||
*/
|
||||
static String readFileFromAssets(AssetManager assetManager, String fileName) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open(fileName)))) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buffer.append(line).append("\n");
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a plaintext file from within the app disk space.
|
||||
*
|
||||
* @param file The file to read.
|
||||
* @return The contents of the file path.
|
||||
* @throws IOException Thrown if any issues reading the provided file path.
|
||||
*/
|
||||
static String readFileFromDisk(File file) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
buffer.append(line).append("\n");
|
||||
}
|
||||
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value of the data column for this Uri. This is useful for
|
||||
* MediaStore Uris, and other file-based ContentProviders.
|
||||
*
|
||||
* @param context The context.
|
||||
* @param uri The Uri to query.
|
||||
* @param selection (Optional) Filter used in the query.
|
||||
* @param selectionArgs (Optional) Selection arguments used in the query.
|
||||
* @return The value of the _data column, which is typically a file path.
|
||||
*/
|
||||
private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
|
||||
String path = null;
|
||||
Cursor cursor = null;
|
||||
final String column = "_data";
|
||||
final String[] projection = { column };
|
||||
|
||||
try {
|
||||
cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final int index = cursor.getColumnIndexOrThrow(column);
|
||||
path = cursor.getString(index);
|
||||
}
|
||||
} catch (IllegalArgumentException ex) {
|
||||
return getCopyFilePath(uri, context);
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
if (path == null) {
|
||||
return getCopyFilePath(uri, context);
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
private static String getCopyFilePath(Uri uri, Context context) {
|
||||
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
|
||||
int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
cursor.moveToFirst();
|
||||
String name = (cursor.getString(nameIndex));
|
||||
File file = new File(context.getFilesDir(), name);
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
FileOutputStream outputStream = new FileOutputStream(file);
|
||||
int read = 0;
|
||||
int maxBufferSize = 1024 * 1024;
|
||||
int bufferSize = Math.min(inputStream.available(), maxBufferSize);
|
||||
final byte[] buffers = new byte[bufferSize];
|
||||
while ((read = inputStream.read(buffers)) != -1) {
|
||||
outputStream.write(buffers, 0, read);
|
||||
}
|
||||
inputStream.close();
|
||||
outputStream.close();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
} finally {
|
||||
if (cursor != null) cursor.close();
|
||||
}
|
||||
return file.getPath();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is ExternalStorageProvider.
|
||||
*/
|
||||
private static boolean isExternalStorageDocument(Uri uri) {
|
||||
return "com.android.externalstorage.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is DownloadsProvider.
|
||||
*/
|
||||
private static boolean isDownloadsDocument(Uri uri) {
|
||||
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is MediaProvider.
|
||||
*/
|
||||
private static boolean isMediaDocument(Uri uri) {
|
||||
return "com.android.providers.media.documents".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param uri The Uri to check.
|
||||
* @return Whether the Uri authority is Google Photos.
|
||||
*/
|
||||
private static boolean isGooglePhotosUri(Uri uri) {
|
||||
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
|
||||
}
|
||||
|
||||
private static String getPathToNonPrimaryVolume(Context context, String tag) {
|
||||
File[] volumes = context.getExternalCacheDirs();
|
||||
if (volumes != null) {
|
||||
for (File volume : volumes) {
|
||||
if (volume != null) {
|
||||
String path = volume.getAbsolutePath();
|
||||
if (path != null) {
|
||||
int index = path.indexOf(tag);
|
||||
if (index != -1) {
|
||||
return path.substring(0, index) + tag;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
class InvalidPluginException extends Exception {
|
||||
|
||||
public InvalidPluginException(String s) {
|
||||
super(s);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
class InvalidPluginMethodException extends Exception {
|
||||
|
||||
public InvalidPluginMethodException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public InvalidPluginMethodException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
public InvalidPluginMethodException(String s, Throwable t) {
|
||||
super(s, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class JSArray extends JSONArray {
|
||||
|
||||
public JSArray() {
|
||||
super();
|
||||
}
|
||||
|
||||
public JSArray(String json) throws JSONException {
|
||||
super(json);
|
||||
}
|
||||
|
||||
public JSArray(Collection copyFrom) {
|
||||
super(copyFrom);
|
||||
}
|
||||
|
||||
public JSArray(Object array) throws JSONException {
|
||||
super(array);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E> List<E> toList() throws JSONException {
|
||||
List<E> items = new ArrayList<>();
|
||||
Object o = null;
|
||||
for (int i = 0; i < this.length(); i++) {
|
||||
o = this.get(i);
|
||||
try {
|
||||
items.add((E) this.get(i));
|
||||
} catch (Exception ex) {
|
||||
throw new JSONException("Not all items are instances of the given type");
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new JSArray without throwing a error
|
||||
*/
|
||||
public static JSArray from(Object array) {
|
||||
try {
|
||||
return new JSArray(array);
|
||||
} catch (JSONException ex) {}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,193 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import static com.getcapacitor.FileUtils.readFileFromAssets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class JSExport {
|
||||
|
||||
private static String CATCHALL_OPTIONS_PARAM = "_options";
|
||||
private static String CALLBACK_PARAM = "_callback";
|
||||
|
||||
public static String getGlobalJS(Context context, boolean loggingEnabled, boolean isDebug) {
|
||||
return "window.Capacitor = { DEBUG: " + isDebug + ", isLoggingEnabled: " + loggingEnabled + ", Plugins: {} };";
|
||||
}
|
||||
|
||||
public static String getCordovaJS(Context context) {
|
||||
String fileContent = "";
|
||||
try {
|
||||
fileContent = readFileFromAssets(context.getAssets(), "public/cordova.js");
|
||||
} catch (IOException ex) {
|
||||
Logger.error("Unable to read public/cordova.js file, Cordova plugins will not work");
|
||||
}
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
public static String getCordovaPluginsFileJS(Context context) {
|
||||
String fileContent = "";
|
||||
try {
|
||||
fileContent = readFileFromAssets(context.getAssets(), "public/cordova_plugins.js");
|
||||
} catch (IOException ex) {
|
||||
Logger.error("Unable to read public/cordova_plugins.js file, Cordova plugins will not work");
|
||||
}
|
||||
return fileContent;
|
||||
}
|
||||
|
||||
public static String getPluginJS(Collection<PluginHandle> plugins) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
JSONArray pluginArray = new JSONArray();
|
||||
|
||||
lines.add("// Begin: Capacitor Plugin JS");
|
||||
for (PluginHandle plugin : plugins) {
|
||||
lines.add(
|
||||
"(function(w) {\n" +
|
||||
"var a = (w.Capacitor = w.Capacitor || {});\n" +
|
||||
"var p = (a.Plugins = a.Plugins || {});\n" +
|
||||
"var t = (p['" +
|
||||
plugin.getId() +
|
||||
"'] = {});\n" +
|
||||
"t.addListener = function(eventName, callback) {\n" +
|
||||
" return w.Capacitor.addListener('" +
|
||||
plugin.getId() +
|
||||
"', eventName, callback);\n" +
|
||||
"}"
|
||||
);
|
||||
Collection<PluginMethodHandle> methods = plugin.getMethods();
|
||||
for (PluginMethodHandle method : methods) {
|
||||
if (method.getName().equals("addListener") || method.getName().equals("removeListener")) {
|
||||
// Don't export add/remove listener, we do that automatically above as they are "special snowflakes"
|
||||
continue;
|
||||
}
|
||||
lines.add(generateMethodJS(plugin, method));
|
||||
}
|
||||
|
||||
lines.add("})(window);\n");
|
||||
pluginArray.put(createPluginHeader(plugin));
|
||||
}
|
||||
|
||||
return TextUtils.join("\n", lines) + "\nwindow.Capacitor.PluginHeaders = " + pluginArray.toString() + ";";
|
||||
}
|
||||
|
||||
public static String getCordovaPluginJS(Context context) {
|
||||
return getFilesContent(context, "public/plugins");
|
||||
}
|
||||
|
||||
public static String getFilesContent(Context context, String path) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
try {
|
||||
String[] content = context.getAssets().list(path);
|
||||
if (content.length > 0) {
|
||||
for (String file : content) {
|
||||
if (!file.endsWith(".map")) {
|
||||
builder.append(getFilesContent(context, path + "/" + file));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return readFileFromAssets(context.getAssets(), path);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
Logger.warn("Unable to read file at path " + path);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static JSONObject createPluginHeader(PluginHandle plugin) {
|
||||
JSONObject pluginObj = new JSONObject();
|
||||
Collection<PluginMethodHandle> methods = plugin.getMethods();
|
||||
try {
|
||||
String id = plugin.getId();
|
||||
JSONArray methodArray = new JSONArray();
|
||||
pluginObj.put("name", id);
|
||||
|
||||
for (PluginMethodHandle method : methods) {
|
||||
methodArray.put(createPluginMethodHeader(method));
|
||||
}
|
||||
|
||||
pluginObj.put("methods", methodArray);
|
||||
} catch (JSONException e) {
|
||||
// ignore
|
||||
}
|
||||
return pluginObj;
|
||||
}
|
||||
|
||||
private static JSONObject createPluginMethodHeader(PluginMethodHandle method) {
|
||||
JSONObject methodObj = new JSONObject();
|
||||
|
||||
try {
|
||||
methodObj.put("name", method.getName());
|
||||
if (!method.getReturnType().equals(PluginMethod.RETURN_NONE)) {
|
||||
methodObj.put("rtype", method.getReturnType());
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
return methodObj;
|
||||
}
|
||||
|
||||
public static String getBridgeJS(Context context) throws JSExportException {
|
||||
return getFilesContent(context, "native-bridge.js");
|
||||
}
|
||||
|
||||
private static String generateMethodJS(PluginHandle plugin, PluginMethodHandle method) {
|
||||
List<String> lines = new ArrayList<>();
|
||||
|
||||
List<String> args = new ArrayList<>();
|
||||
// Add the catch all param that will take a full javascript object to pass to the plugin
|
||||
args.add(CATCHALL_OPTIONS_PARAM);
|
||||
|
||||
String returnType = method.getReturnType();
|
||||
if (returnType.equals(PluginMethod.RETURN_CALLBACK)) {
|
||||
args.add(CALLBACK_PARAM);
|
||||
}
|
||||
|
||||
// Create the method function declaration
|
||||
lines.add("t['" + method.getName() + "'] = function(" + TextUtils.join(", ", args) + ") {");
|
||||
|
||||
switch (returnType) {
|
||||
case PluginMethod.RETURN_NONE:
|
||||
lines.add(
|
||||
"return w.Capacitor.nativeCallback('" +
|
||||
plugin.getId() +
|
||||
"', '" +
|
||||
method.getName() +
|
||||
"', " +
|
||||
CATCHALL_OPTIONS_PARAM +
|
||||
")"
|
||||
);
|
||||
break;
|
||||
case PluginMethod.RETURN_PROMISE:
|
||||
lines.add(
|
||||
"return w.Capacitor.nativePromise('" + plugin.getId() + "', '" + method.getName() + "', " + CATCHALL_OPTIONS_PARAM + ")"
|
||||
);
|
||||
break;
|
||||
case PluginMethod.RETURN_CALLBACK:
|
||||
lines.add(
|
||||
"return w.Capacitor.nativeCallback('" +
|
||||
plugin.getId() +
|
||||
"', '" +
|
||||
method.getName() +
|
||||
"', " +
|
||||
CATCHALL_OPTIONS_PARAM +
|
||||
", " +
|
||||
CALLBACK_PARAM +
|
||||
")"
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// TODO: Do something here?
|
||||
}
|
||||
|
||||
lines.add("}");
|
||||
|
||||
return TextUtils.join("\n", lines);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
public class JSExportException extends Exception {
|
||||
|
||||
public JSExportException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public JSExportException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
public JSExportException(String s, Throwable t) {
|
||||
super(s, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* JSInject is responsible for returning Capacitor's core
|
||||
* runtime JS and any plugin JS back into HTML page responses
|
||||
* to the client.
|
||||
*/
|
||||
class JSInjector {
|
||||
|
||||
private String globalJS;
|
||||
private String bridgeJS;
|
||||
private String pluginJS;
|
||||
private String cordovaJS;
|
||||
private String cordovaPluginsJS;
|
||||
private String cordovaPluginsFileJS;
|
||||
private String localUrlJS;
|
||||
|
||||
public JSInjector(
|
||||
String globalJS,
|
||||
String bridgeJS,
|
||||
String pluginJS,
|
||||
String cordovaJS,
|
||||
String cordovaPluginsJS,
|
||||
String cordovaPluginsFileJS,
|
||||
String localUrlJS
|
||||
) {
|
||||
this.globalJS = globalJS;
|
||||
this.bridgeJS = bridgeJS;
|
||||
this.pluginJS = pluginJS;
|
||||
this.cordovaJS = cordovaJS;
|
||||
this.cordovaPluginsJS = cordovaPluginsJS;
|
||||
this.cordovaPluginsFileJS = cordovaPluginsFileJS;
|
||||
this.localUrlJS = localUrlJS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates injectable JS content.
|
||||
* This may be used in other forms of injecting that aren't using an InputStream.
|
||||
* @return
|
||||
*/
|
||||
public String getScriptString() {
|
||||
return (
|
||||
globalJS +
|
||||
"\n\n" +
|
||||
localUrlJS +
|
||||
"\n\n" +
|
||||
bridgeJS +
|
||||
"\n\n" +
|
||||
pluginJS +
|
||||
"\n\n" +
|
||||
cordovaJS +
|
||||
"\n\n" +
|
||||
cordovaPluginsFileJS +
|
||||
"\n\n" +
|
||||
cordovaPluginsJS
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an InputStream from the web server, prepend it with
|
||||
* our JS stream
|
||||
* @param responseStream
|
||||
* @return
|
||||
*/
|
||||
public InputStream getInjectedStream(InputStream responseStream) {
|
||||
String js = "<script type=\"text/javascript\">" + getScriptString() + "</script>";
|
||||
String html = this.readAssetStream(responseStream);
|
||||
|
||||
// Insert the js string at the position after <head> or before </head> using StringBuilder
|
||||
StringBuilder modifiedHtml = new StringBuilder(html);
|
||||
if (html.contains("<head>")) {
|
||||
modifiedHtml.insert(html.indexOf("<head>") + "<head>".length(), "\n" + js + "\n");
|
||||
html = modifiedHtml.toString();
|
||||
} else if (html.contains("</head>")) {
|
||||
modifiedHtml.insert(html.indexOf("</head>"), "\n" + js + "\n");
|
||||
html = modifiedHtml.toString();
|
||||
} else {
|
||||
Logger.error("Unable to inject Capacitor, Plugins won't work");
|
||||
}
|
||||
return new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String readAssetStream(InputStream stream) {
|
||||
try {
|
||||
final int bufferSize = 1024;
|
||||
final char[] buffer = new char[bufferSize];
|
||||
final StringBuilder out = new StringBuilder();
|
||||
Reader in = new InputStreamReader(stream, StandardCharsets.UTF_8);
|
||||
for (;;) {
|
||||
int rsz = in.read(buffer, 0, buffer.length);
|
||||
if (rsz < 0) break;
|
||||
out.append(buffer, 0, rsz);
|
||||
}
|
||||
return out.toString();
|
||||
} catch (Exception e) {
|
||||
Logger.error("Unable to process HTML asset file. This is a fatal error", e);
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* A wrapper around JSONObject that isn't afraid to do simple
|
||||
* JSON put operations without having to throw an exception
|
||||
* for every little thing jeez
|
||||
*/
|
||||
public class JSObject extends JSONObject {
|
||||
|
||||
public JSObject() {
|
||||
super();
|
||||
}
|
||||
|
||||
public JSObject(String json) throws JSONException {
|
||||
super(json);
|
||||
}
|
||||
|
||||
public JSObject(JSONObject obj, String[] names) throws JSONException {
|
||||
super(obj, names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a pathetic JSONObject into a JSObject
|
||||
* @param obj
|
||||
*/
|
||||
public static JSObject fromJSONObject(JSONObject obj) throws JSONException {
|
||||
Iterator<String> keysIter = obj.keys();
|
||||
List<String> keys = new ArrayList<>();
|
||||
while (keysIter.hasNext()) {
|
||||
keys.add(keysIter.next());
|
||||
}
|
||||
|
||||
return new JSObject(obj, keys.toArray(new String[keys.size()]));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getString(String key) {
|
||||
return getString(key, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getString(String key, @Nullable String defaultValue) {
|
||||
try {
|
||||
String value = super.getString(key);
|
||||
if (!super.isNull(key)) {
|
||||
return value;
|
||||
}
|
||||
} catch (JSONException ex) {}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getInteger(String key) {
|
||||
return getInteger(key, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getInteger(String key, @Nullable Integer defaultValue) {
|
||||
try {
|
||||
return super.getInt(key);
|
||||
} catch (JSONException e) {}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getBoolean(String key, @Nullable Boolean defaultValue) {
|
||||
try {
|
||||
return super.getBoolean(key);
|
||||
} catch (JSONException e) {}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch boolean from jsonObject
|
||||
*/
|
||||
@Nullable
|
||||
public Boolean getBool(String key) {
|
||||
return getBoolean(key, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public JSObject getJSObject(String name) {
|
||||
try {
|
||||
return getJSObject(name, null);
|
||||
} catch (JSONException e) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public JSObject getJSObject(String name, @Nullable JSObject defaultValue) throws JSONException {
|
||||
try {
|
||||
Object obj = get(name);
|
||||
if (obj instanceof JSONObject) {
|
||||
Iterator<String> keysIter = ((JSONObject) obj).keys();
|
||||
List<String> keys = new ArrayList<>();
|
||||
while (keysIter.hasNext()) {
|
||||
keys.add(keysIter.next());
|
||||
}
|
||||
|
||||
return new JSObject((JSONObject) obj, keys.toArray(new String[keys.size()]));
|
||||
}
|
||||
} catch (JSONException ex) {}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSObject put(String key, boolean value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSObject put(String key, int value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSObject put(String key, long value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSObject put(String key, double value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSObject put(String key, Object value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObject put(String key, String value) {
|
||||
try {
|
||||
super.put(key, value);
|
||||
} catch (JSONException ex) {}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObject putSafe(String key, Object value) throws JSONException {
|
||||
return (JSObject) super.put(key, value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import org.json.JSONException;
|
||||
|
||||
/**
|
||||
* Represents a single user-data value of any type on the capacitor PluginCall object.
|
||||
*/
|
||||
public class JSValue {
|
||||
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* @param call The capacitor plugin call, used for accessing the value safely.
|
||||
* @param name The name of the property to access.
|
||||
*/
|
||||
public JSValue(PluginCall call, String name) {
|
||||
this.value = this.toValue(call, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the coerced but uncasted underlying value.
|
||||
*/
|
||||
public Object getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.getValue().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying value as a JSObject, or throwing if it cannot.
|
||||
*
|
||||
* @throws JSONException If the underlying value is not a JSObject.
|
||||
*/
|
||||
public JSObject toJSObject() throws JSONException {
|
||||
if (this.value instanceof JSObject) return (JSObject) this.value;
|
||||
throw new JSONException("JSValue could not be coerced to JSObject.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying value as a JSArray, or throwing if it cannot.
|
||||
*
|
||||
* @throws JSONException If the underlying value is not a JSArray.
|
||||
*/
|
||||
public JSArray toJSArray() throws JSONException {
|
||||
if (this.value instanceof JSArray) return (JSArray) this.value;
|
||||
throw new JSONException("JSValue could not be coerced to JSArray.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported.
|
||||
*/
|
||||
private Object toValue(PluginCall call, String name) {
|
||||
Object value = null;
|
||||
value = call.getArray(name, null);
|
||||
if (value != null) return value;
|
||||
value = call.getObject(name, null);
|
||||
if (value != null) return value;
|
||||
value = call.getString(name, null);
|
||||
if (value != null) return value;
|
||||
return call.getData().opt(name);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
|
||||
public class Logger {
|
||||
|
||||
public static final String LOG_TAG_CORE = "Capacitor";
|
||||
public static CapConfig config;
|
||||
|
||||
private static Logger instance;
|
||||
|
||||
private static Logger getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new Logger();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public static void init(CapConfig config) {
|
||||
Logger.getInstance().loadConfig(config);
|
||||
}
|
||||
|
||||
private void loadConfig(CapConfig config) {
|
||||
Logger.config = config;
|
||||
}
|
||||
|
||||
public static String tags(String... subtags) {
|
||||
if (subtags != null && subtags.length > 0) {
|
||||
return LOG_TAG_CORE + "/" + TextUtils.join("/", subtags);
|
||||
}
|
||||
|
||||
return LOG_TAG_CORE;
|
||||
}
|
||||
|
||||
public static void verbose(String message) {
|
||||
verbose(LOG_TAG_CORE, message);
|
||||
}
|
||||
|
||||
public static void verbose(String tag, String message) {
|
||||
if (!shouldLog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.v(tag, message);
|
||||
}
|
||||
|
||||
public static void debug(String message) {
|
||||
debug(LOG_TAG_CORE, message);
|
||||
}
|
||||
|
||||
public static void debug(String tag, String message) {
|
||||
if (!shouldLog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(tag, message);
|
||||
}
|
||||
|
||||
public static void info(String message) {
|
||||
info(LOG_TAG_CORE, message);
|
||||
}
|
||||
|
||||
public static void info(String tag, String message) {
|
||||
if (!shouldLog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.i(tag, message);
|
||||
}
|
||||
|
||||
public static void warn(String message) {
|
||||
warn(LOG_TAG_CORE, message);
|
||||
}
|
||||
|
||||
public static void warn(String tag, String message) {
|
||||
if (!shouldLog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.w(tag, message);
|
||||
}
|
||||
|
||||
public static void error(String message) {
|
||||
error(LOG_TAG_CORE, message, null);
|
||||
}
|
||||
|
||||
public static void error(String message, Throwable e) {
|
||||
error(LOG_TAG_CORE, message, e);
|
||||
}
|
||||
|
||||
public static void error(String tag, String message, Throwable e) {
|
||||
if (!shouldLog()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log.e(tag, message, e);
|
||||
}
|
||||
|
||||
public static boolean shouldLog() {
|
||||
return config == null || config.isLoggingEnabled();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebView;
|
||||
import androidx.webkit.JavaScriptReplyProxy;
|
||||
import androidx.webkit.WebViewCompat;
|
||||
import androidx.webkit.WebViewFeature;
|
||||
import org.apache.cordova.PluginManager;
|
||||
|
||||
/**
|
||||
* MessageHandler handles messages from the WebView, dispatching them
|
||||
* to plugins.
|
||||
*/
|
||||
public class MessageHandler {
|
||||
|
||||
private Bridge bridge;
|
||||
private WebView webView;
|
||||
private PluginManager cordovaPluginManager;
|
||||
private JavaScriptReplyProxy javaScriptReplyProxy;
|
||||
|
||||
public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) {
|
||||
this.bridge = bridge;
|
||||
this.webView = webView;
|
||||
this.cordovaPluginManager = cordovaPluginManager;
|
||||
|
||||
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) {
|
||||
WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
|
||||
if (isMainFrame) {
|
||||
postMessage(message.getData());
|
||||
javaScriptReplyProxy = replyProxy;
|
||||
} else {
|
||||
Logger.warn("Plugin execution is allowed in Main Frame only");
|
||||
}
|
||||
};
|
||||
try {
|
||||
WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener);
|
||||
} catch (Exception ex) {
|
||||
webView.addJavascriptInterface(this, "androidBridge");
|
||||
}
|
||||
} else {
|
||||
webView.addJavascriptInterface(this, "androidBridge");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main message handler that will be called from JavaScript
|
||||
* to send a message to the native bridge.
|
||||
* @param jsonStr
|
||||
*/
|
||||
@JavascriptInterface
|
||||
@SuppressWarnings("unused")
|
||||
public void postMessage(String jsonStr) {
|
||||
try {
|
||||
JSObject postData = new JSObject(jsonStr);
|
||||
|
||||
String type = postData.getString("type");
|
||||
|
||||
boolean typeIsNotNull = type != null;
|
||||
boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova");
|
||||
boolean isJavaScriptError = typeIsNotNull && type.equals("js.error");
|
||||
|
||||
String callbackId = postData.getString("callbackId");
|
||||
|
||||
if (isCordovaPlugin) {
|
||||
String service = postData.getString("service");
|
||||
String action = postData.getString("action");
|
||||
String actionArgs = postData.getString("actionArgs");
|
||||
|
||||
Logger.verbose(
|
||||
Logger.tags("Plugin"),
|
||||
"To native (Cordova plugin): callbackId: " +
|
||||
callbackId +
|
||||
", service: " +
|
||||
service +
|
||||
", action: " +
|
||||
action +
|
||||
", actionArgs: " +
|
||||
actionArgs
|
||||
);
|
||||
|
||||
this.callCordovaPluginMethod(callbackId, service, action, actionArgs);
|
||||
} else if (isJavaScriptError) {
|
||||
Logger.error("JavaScript Error: " + jsonStr);
|
||||
} else {
|
||||
String pluginId = postData.getString("pluginId");
|
||||
String methodName = postData.getString("methodName");
|
||||
JSObject methodData = postData.getJSObject("options", new JSObject());
|
||||
|
||||
Logger.verbose(
|
||||
Logger.tags("Plugin"),
|
||||
"To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName
|
||||
);
|
||||
|
||||
this.callPluginMethod(callbackId, pluginId, methodName, methodData);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.error("Post message error:", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) {
|
||||
try {
|
||||
PluginResult data = new PluginResult();
|
||||
data.put("save", call.isKeptAlive());
|
||||
data.put("callbackId", call.getCallbackId());
|
||||
data.put("pluginId", call.getPluginId());
|
||||
data.put("methodName", call.getMethodName());
|
||||
|
||||
boolean pluginResultInError = errorResult != null;
|
||||
if (pluginResultInError) {
|
||||
data.put("success", false);
|
||||
data.put("error", errorResult);
|
||||
Logger.debug("Sending plugin error: " + data.toString());
|
||||
} else {
|
||||
data.put("success", true);
|
||||
if (successResult != null) {
|
||||
data.put("data", successResult);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING);
|
||||
if (isValidCallbackId) {
|
||||
if (bridge.getConfig().isUsingLegacyBridge()) {
|
||||
legacySendResponseMessage(data);
|
||||
} else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) {
|
||||
javaScriptReplyProxy.postMessage(data.toString());
|
||||
} else {
|
||||
legacySendResponseMessage(data);
|
||||
}
|
||||
} else {
|
||||
bridge.getApp().fireRestoredResult(data);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.error("sendResponseMessage: error: " + ex);
|
||||
}
|
||||
if (!call.isKeptAlive()) {
|
||||
call.release(bridge);
|
||||
}
|
||||
}
|
||||
|
||||
private void legacySendResponseMessage(PluginResult data) {
|
||||
final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")";
|
||||
final WebView webView = this.webView;
|
||||
webView.post(() -> webView.evaluateJavascript(runScript, null));
|
||||
}
|
||||
|
||||
private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) {
|
||||
PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData);
|
||||
bridge.callPluginMethod(pluginId, methodName, call);
|
||||
}
|
||||
|
||||
private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) {
|
||||
bridge.execute(
|
||||
() -> {
|
||||
cordovaPluginManager.exec(service, action, callbackId, actionArgs);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Base annotation for all Plugins
|
||||
* @deprecated
|
||||
* <p> Use {@link CapacitorPlugin} instead
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Deprecated
|
||||
public @interface NativePlugin {
|
||||
/**
|
||||
* Request codes this plugin uses and responds to, in order to tie
|
||||
* Android events back the plugin to handle
|
||||
*/
|
||||
int[] requestCodes() default {};
|
||||
|
||||
/**
|
||||
* Permissions this plugin needs, in order to make permission requests
|
||||
* easy if the plugin only needs basic permission prompting
|
||||
*/
|
||||
String[] permissions() default {};
|
||||
|
||||
/**
|
||||
* The request code to use when automatically requesting permissions
|
||||
*/
|
||||
int permissionRequestCode() default 9000;
|
||||
|
||||
/**
|
||||
* A custom name for the plugin, otherwise uses the
|
||||
* simple class name.
|
||||
*/
|
||||
String name() default "";
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Represents the state of a permission
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public enum PermissionState {
|
||||
GRANTED("granted"),
|
||||
DENIED("denied"),
|
||||
PROMPT("prompt"),
|
||||
PROMPT_WITH_RATIONALE("prompt-with-rationale");
|
||||
|
||||
private String state;
|
||||
|
||||
PermissionState(String state) {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public static PermissionState byState(String state) {
|
||||
state = state.toUpperCase(Locale.ROOT).replace('-', '_');
|
||||
return valueOf(state);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -0,0 +1,440 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Wraps a call from the web layer to native
|
||||
*/
|
||||
public class PluginCall {
|
||||
|
||||
/**
|
||||
* A special callback id that indicates there is no matching callback
|
||||
* on the client to associate any PluginCall results back to. This is used
|
||||
* in the case of an app resuming with saved instance data, for example.
|
||||
*/
|
||||
public static final String CALLBACK_ID_DANGLING = "-1";
|
||||
|
||||
private final MessageHandler msgHandler;
|
||||
private final String pluginId;
|
||||
private final String callbackId;
|
||||
private final String methodName;
|
||||
private final JSObject data;
|
||||
|
||||
private boolean keepAlive = false;
|
||||
|
||||
/**
|
||||
* Indicates that this PluginCall was released, and should no longer be used
|
||||
*/
|
||||
@Deprecated
|
||||
private boolean isReleased = false;
|
||||
|
||||
public PluginCall(MessageHandler msgHandler, String pluginId, String callbackId, String methodName, JSObject data) {
|
||||
this.msgHandler = msgHandler;
|
||||
this.pluginId = pluginId;
|
||||
this.callbackId = callbackId;
|
||||
this.methodName = methodName;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void successCallback(PluginResult successResult) {
|
||||
if (CALLBACK_ID_DANGLING.equals(this.callbackId)) {
|
||||
// don't send back response if the callbackId was "-1"
|
||||
return;
|
||||
}
|
||||
|
||||
this.msgHandler.sendResponseMessage(this, successResult, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #resolve(JSObject data)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void success(JSObject data) {
|
||||
PluginResult result = new PluginResult(data);
|
||||
this.msgHandler.sendResponseMessage(this, result, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #resolve()}
|
||||
*/
|
||||
@Deprecated
|
||||
public void success() {
|
||||
this.resolve(new JSObject());
|
||||
}
|
||||
|
||||
public void resolve(JSObject data) {
|
||||
PluginResult result = new PluginResult(data);
|
||||
this.msgHandler.sendResponseMessage(this, result, null);
|
||||
}
|
||||
|
||||
public void resolve() {
|
||||
this.msgHandler.sendResponseMessage(this, null, null);
|
||||
}
|
||||
|
||||
public void errorCallback(String msg) {
|
||||
PluginResult errorResult = new PluginResult();
|
||||
|
||||
try {
|
||||
errorResult.put("message", msg);
|
||||
} catch (Exception jsonEx) {
|
||||
Logger.error(Logger.tags("Plugin"), jsonEx.toString(), null);
|
||||
}
|
||||
|
||||
this.msgHandler.sendResponseMessage(this, null, errorResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #reject(String msg, Exception ex)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void error(String msg, Exception ex) {
|
||||
reject(msg, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #reject(String msg, String code, Exception ex)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void error(String msg, String code, Exception ex) {
|
||||
reject(msg, code, ex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Use {@link #reject(String msg)}
|
||||
*/
|
||||
@Deprecated
|
||||
public void error(String msg) {
|
||||
reject(msg);
|
||||
}
|
||||
|
||||
public void reject(String msg, String code, Exception ex, JSObject data) {
|
||||
PluginResult errorResult = new PluginResult();
|
||||
|
||||
if (ex != null) {
|
||||
Logger.error(Logger.tags("Plugin"), msg, ex);
|
||||
}
|
||||
|
||||
try {
|
||||
errorResult.put("message", msg);
|
||||
errorResult.put("code", code);
|
||||
if (null != data) {
|
||||
errorResult.put("data", data);
|
||||
}
|
||||
} catch (Exception jsonEx) {
|
||||
Logger.error(Logger.tags("Plugin"), jsonEx.getMessage(), jsonEx);
|
||||
}
|
||||
|
||||
this.msgHandler.sendResponseMessage(this, null, errorResult);
|
||||
}
|
||||
|
||||
public void reject(String msg, Exception ex, JSObject data) {
|
||||
reject(msg, null, ex, data);
|
||||
}
|
||||
|
||||
public void reject(String msg, String code, JSObject data) {
|
||||
reject(msg, code, null, data);
|
||||
}
|
||||
|
||||
public void reject(String msg, String code, Exception ex) {
|
||||
reject(msg, code, ex, null);
|
||||
}
|
||||
|
||||
public void reject(String msg, JSObject data) {
|
||||
reject(msg, null, null, data);
|
||||
}
|
||||
|
||||
public void reject(String msg, Exception ex) {
|
||||
reject(msg, null, ex, null);
|
||||
}
|
||||
|
||||
public void reject(String msg, String code) {
|
||||
reject(msg, code, null, null);
|
||||
}
|
||||
|
||||
public void reject(String msg) {
|
||||
reject(msg, null, null, null);
|
||||
}
|
||||
|
||||
public void unimplemented() {
|
||||
unimplemented("not implemented");
|
||||
}
|
||||
|
||||
public void unimplemented(String msg) {
|
||||
reject(msg, "UNIMPLEMENTED", null, null);
|
||||
}
|
||||
|
||||
public void unavailable() {
|
||||
unavailable("not available");
|
||||
}
|
||||
|
||||
public void unavailable(String msg) {
|
||||
reject(msg, "UNAVAILABLE", null, null);
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return this.pluginId;
|
||||
}
|
||||
|
||||
public String getCallbackId() {
|
||||
return this.callbackId;
|
||||
}
|
||||
|
||||
public String getMethodName() {
|
||||
return this.methodName;
|
||||
}
|
||||
|
||||
public JSObject getData() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getString(String name) {
|
||||
return this.getString(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getString(String name, @Nullable String defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getInt(String name) {
|
||||
return this.getInt(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getInt(String name, @Nullable Integer defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getLong(String name) {
|
||||
return this.getLong(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Long getLong(String name, @Nullable Long defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof Long) {
|
||||
return (Long) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Float getFloat(String name) {
|
||||
return this.getFloat(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Float getFloat(String name, @Nullable Float defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof Float) {
|
||||
return (Float) value;
|
||||
}
|
||||
if (value instanceof Double) {
|
||||
return ((Double) value).floatValue();
|
||||
}
|
||||
if (value instanceof Integer) {
|
||||
return ((Integer) value).floatValue();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getDouble(String name) {
|
||||
return this.getDouble(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Double getDouble(String name, @Nullable Double defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof Double) {
|
||||
return (Double) value;
|
||||
}
|
||||
if (value instanceof Float) {
|
||||
return ((Float) value).doubleValue();
|
||||
}
|
||||
if (value instanceof Integer) {
|
||||
return ((Integer) value).doubleValue();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getBoolean(String name) {
|
||||
return this.getBoolean(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Boolean getBoolean(String name, @Nullable Boolean defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public JSObject getObject(String name) {
|
||||
return this.getObject(name, null);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public JSObject getObject(String name, JSObject defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof JSONObject) {
|
||||
try {
|
||||
return JSObject.fromJSONObject((JSONObject) value);
|
||||
} catch (JSONException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public JSArray getArray(String name) {
|
||||
return this.getArray(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSONArray and turn it into a JSArray
|
||||
* @param name
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
@Nullable
|
||||
public JSArray getArray(String name, JSArray defaultValue) {
|
||||
Object value = this.data.opt(name);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (value instanceof JSONArray) {
|
||||
try {
|
||||
JSONArray valueArray = (JSONArray) value;
|
||||
List<Object> items = new ArrayList<>();
|
||||
for (int i = 0; i < valueArray.length(); i++) {
|
||||
items.add(valueArray.get(i));
|
||||
}
|
||||
return new JSArray(items.toArray());
|
||||
} catch (JSONException ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param name of the option to check
|
||||
* @return boolean indicating if the plugin call has an option for the provided name.
|
||||
* @deprecated Presence of a key should not be considered significant.
|
||||
* Use typed accessors to check the value instead.
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean hasOption(String name) {
|
||||
return this.data.has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the Bridge should cache this call in order to call
|
||||
* it again later. For example, the addListener system uses this to
|
||||
* continuously call the call's callback (😆).
|
||||
* @deprecated use {@link #setKeepAlive(Boolean)} instead
|
||||
*/
|
||||
@Deprecated
|
||||
public void save() {
|
||||
setKeepAlive(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the Bridge should cache this call in order to call
|
||||
* it again later. For example, the addListener system uses this to
|
||||
* continuously call the call's callback.
|
||||
*
|
||||
* @param keepAlive whether to keep the callback saved
|
||||
*/
|
||||
public void setKeepAlive(Boolean keepAlive) {
|
||||
this.keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
public void release(Bridge bridge) {
|
||||
this.keepAlive = false;
|
||||
bridge.releaseCall(this);
|
||||
this.isReleased = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use {@link #isKeptAlive()}
|
||||
* @return true if the plugin call is kept alive
|
||||
*/
|
||||
@Deprecated
|
||||
public boolean isSaved() {
|
||||
return isKeptAlive();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the keepAlive value of the plugin call
|
||||
* @return true if the plugin call is kept alive
|
||||
*/
|
||||
public boolean isKeptAlive() {
|
||||
return keepAlive;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public boolean isReleased() {
|
||||
return isReleased;
|
||||
}
|
||||
|
||||
class PluginCallDataTypeException extends Exception {
|
||||
|
||||
PluginCallDataTypeException(String m) {
|
||||
super(m);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import com.getcapacitor.util.JSONUtils;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Represents the configuration options for plugins used by Capacitor
|
||||
*/
|
||||
public class PluginConfig {
|
||||
|
||||
/**
|
||||
* The object containing plugin config values.
|
||||
*/
|
||||
private final JSONObject config;
|
||||
|
||||
/**
|
||||
* Constructs a PluginsConfig with the provided JSONObject value.
|
||||
*
|
||||
* @param config A plugin configuration expressed as a JSON Object
|
||||
*/
|
||||
PluginConfig(JSONObject config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
public String getString(String configKey) {
|
||||
return getString(configKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
public String getString(String configKey, String defaultValue) {
|
||||
return JSONUtils.getString(config, configKey, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a boolean value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
public boolean getBoolean(String configKey, boolean defaultValue) {
|
||||
return JSONUtils.getBoolean(config, configKey, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an integer value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
public int getInt(String configKey, int defaultValue) {
|
||||
return JSONUtils.getInt(config, configKey, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string array value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
public String[] getArray(String configKey) {
|
||||
return getArray(configKey, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string array value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @param defaultValue A default value to return if the key does not exist in the config
|
||||
* @return The value from the config, if key exists. Default value returned if not
|
||||
*/
|
||||
public String[] getArray(String configKey, String[] defaultValue) {
|
||||
return JSONUtils.getArray(config, configKey, defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON object value for a plugin in the Capacitor config.
|
||||
*
|
||||
* @param configKey The key of the value to retrieve
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
public JSONObject getObject(String configKey) {
|
||||
return JSONUtils.getObject(config, configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the PluginConfig is empty.
|
||||
*
|
||||
* @return true if the plugin config has no entries
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return config.length() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the JSON Object containing the config of the the provided plugin ID.
|
||||
*
|
||||
* @return The config for that plugin
|
||||
*/
|
||||
public JSONObject getConfigJSON() {
|
||||
return config;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* PluginHandle is an instance of a plugin that has been registered
|
||||
* and indexed. Think of it as a Plugin instance with extra metadata goodies
|
||||
*/
|
||||
public class PluginHandle {
|
||||
|
||||
private final Bridge bridge;
|
||||
private final Class<? extends Plugin> pluginClass;
|
||||
|
||||
private final Map<String, PluginMethodHandle> pluginMethods = new HashMap<>();
|
||||
|
||||
private final String pluginId;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private NativePlugin legacyPluginAnnotation;
|
||||
|
||||
private CapacitorPlugin pluginAnnotation;
|
||||
|
||||
private Plugin instance;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private PluginHandle(Class<? extends Plugin> clazz, Bridge bridge) throws InvalidPluginException {
|
||||
this.bridge = bridge;
|
||||
this.pluginClass = clazz;
|
||||
|
||||
CapacitorPlugin pluginAnnotation = pluginClass.getAnnotation(CapacitorPlugin.class);
|
||||
if (pluginAnnotation == null) {
|
||||
// Check for legacy plugin annotation, @NativePlugin
|
||||
NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class);
|
||||
if (legacyPluginAnnotation == null) {
|
||||
throw new InvalidPluginException("No @CapacitorPlugin annotation found for plugin " + pluginClass.getName());
|
||||
}
|
||||
|
||||
if (!legacyPluginAnnotation.name().equals("")) {
|
||||
this.pluginId = legacyPluginAnnotation.name();
|
||||
} else {
|
||||
this.pluginId = pluginClass.getSimpleName();
|
||||
}
|
||||
|
||||
this.legacyPluginAnnotation = legacyPluginAnnotation;
|
||||
} else {
|
||||
if (!pluginAnnotation.name().equals("")) {
|
||||
this.pluginId = pluginAnnotation.name();
|
||||
} else {
|
||||
this.pluginId = pluginClass.getSimpleName();
|
||||
}
|
||||
|
||||
this.pluginAnnotation = pluginAnnotation;
|
||||
}
|
||||
|
||||
this.indexMethods(clazz);
|
||||
}
|
||||
|
||||
public PluginHandle(Bridge bridge, Class<? extends Plugin> pluginClass) throws InvalidPluginException, PluginLoadException {
|
||||
this(pluginClass, bridge);
|
||||
this.load();
|
||||
}
|
||||
|
||||
public PluginHandle(Bridge bridge, Plugin plugin) throws InvalidPluginException {
|
||||
this(plugin.getClass(), bridge);
|
||||
this.loadInstance(plugin);
|
||||
}
|
||||
|
||||
public Class<? extends Plugin> getPluginClass() {
|
||||
return pluginClass;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return this.pluginId;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public NativePlugin getLegacyPluginAnnotation() {
|
||||
return this.legacyPluginAnnotation;
|
||||
}
|
||||
|
||||
public CapacitorPlugin getPluginAnnotation() {
|
||||
return this.pluginAnnotation;
|
||||
}
|
||||
|
||||
public Plugin getInstance() {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
public Collection<PluginMethodHandle> getMethods() {
|
||||
return this.pluginMethods.values();
|
||||
}
|
||||
|
||||
public Plugin load() throws PluginLoadException {
|
||||
if (this.instance != null) {
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
try {
|
||||
this.instance = this.pluginClass.getDeclaredConstructor().newInstance();
|
||||
return this.loadInstance(instance);
|
||||
} catch (Exception ex) {
|
||||
throw new PluginLoadException("Unable to load plugin instance. Ensure plugin is publicly accessible");
|
||||
}
|
||||
}
|
||||
|
||||
public Plugin loadInstance(Plugin plugin) {
|
||||
this.instance = plugin;
|
||||
this.instance.setPluginHandle(this);
|
||||
this.instance.setBridge(this.bridge);
|
||||
this.instance.load();
|
||||
this.instance.initializeActivityLaunchers();
|
||||
return this.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call a method on a plugin.
|
||||
* @param methodName the name of the method to call
|
||||
* @param call the constructed PluginCall with parameters from the caller
|
||||
* @throws InvalidPluginMethodException if no method was found on that plugin
|
||||
*/
|
||||
public void invoke(String methodName, PluginCall call)
|
||||
throws PluginLoadException, InvalidPluginMethodException, InvocationTargetException, IllegalAccessException {
|
||||
if (this.instance == null) {
|
||||
// Can throw PluginLoadException
|
||||
this.load();
|
||||
}
|
||||
|
||||
PluginMethodHandle methodMeta = pluginMethods.get(methodName);
|
||||
if (methodMeta == null) {
|
||||
throw new InvalidPluginMethodException("No method " + methodName + " found for plugin " + pluginClass.getName());
|
||||
}
|
||||
|
||||
methodMeta.getMethod().invoke(this.instance, call);
|
||||
}
|
||||
|
||||
/**
|
||||
* Index all the known callable methods for a plugin for faster
|
||||
* invocation later
|
||||
*/
|
||||
private void indexMethods(Class<? extends Plugin> plugin) {
|
||||
//Method[] methods = pluginClass.getDeclaredMethods();
|
||||
Method[] methods = pluginClass.getMethods();
|
||||
|
||||
for (Method methodReflect : methods) {
|
||||
PluginMethod method = methodReflect.getAnnotation(PluginMethod.class);
|
||||
|
||||
if (method == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PluginMethodHandle methodMeta = new PluginMethodHandle(methodReflect, method);
|
||||
pluginMethods.put(methodReflect.getName(), methodMeta);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
class PluginInvocationException extends Exception {
|
||||
|
||||
public PluginInvocationException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public PluginInvocationException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
public PluginInvocationException(String s, Throwable t) {
|
||||
super(s, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
/**
|
||||
* Thrown when a plugin fails to instantiate
|
||||
*/
|
||||
public class PluginLoadException extends Exception {
|
||||
|
||||
public PluginLoadException(String s) {
|
||||
super(s);
|
||||
}
|
||||
|
||||
public PluginLoadException(Throwable t) {
|
||||
super(t);
|
||||
}
|
||||
|
||||
public PluginLoadException(String s, Throwable t) {
|
||||
super(s, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.content.res.AssetManager;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class PluginManager {
|
||||
|
||||
private final AssetManager assetManager;
|
||||
|
||||
public PluginManager(AssetManager assetManager) {
|
||||
this.assetManager = assetManager;
|
||||
}
|
||||
|
||||
public List<Class<? extends Plugin>> loadPluginClasses() throws PluginLoadException {
|
||||
JSONArray pluginsJSON = parsePluginsJSON();
|
||||
ArrayList<Class<? extends Plugin>> pluginList = new ArrayList<>();
|
||||
|
||||
try {
|
||||
for (int i = 0, size = pluginsJSON.length(); i < size; i++) {
|
||||
JSONObject pluginJSON = pluginsJSON.getJSONObject(i);
|
||||
String classPath = pluginJSON.getString("classpath");
|
||||
Class<?> c = Class.forName(classPath);
|
||||
pluginList.add(c.asSubclass(Plugin.class));
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new PluginLoadException("Could not find class by class path: " + e.getMessage());
|
||||
}
|
||||
|
||||
return pluginList;
|
||||
}
|
||||
|
||||
private JSONArray parsePluginsJSON() throws PluginLoadException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open("capacitor.plugins.json")))) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
builder.append(line);
|
||||
}
|
||||
String jsonString = builder.toString();
|
||||
return new JSONArray(jsonString);
|
||||
} catch (IOException e) {
|
||||
throw new PluginLoadException("Could not load capacitor.plugins.json");
|
||||
} catch (JSONException e) {
|
||||
throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface PluginMethod {
|
||||
String RETURN_PROMISE = "promise";
|
||||
|
||||
String RETURN_CALLBACK = "callback";
|
||||
|
||||
String RETURN_NONE = "none";
|
||||
|
||||
String returnType() default RETURN_PROMISE;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
public class PluginMethodHandle {
|
||||
|
||||
// The reflect method reference
|
||||
private final Method method;
|
||||
// The name of the method
|
||||
private final String name;
|
||||
// The return type of the method (see PluginMethod for constants)
|
||||
private final String returnType;
|
||||
|
||||
public PluginMethodHandle(Method method, PluginMethod methodDecorator) {
|
||||
this.method = method;
|
||||
|
||||
this.name = method.getName();
|
||||
|
||||
this.returnType = methodDecorator.returnType();
|
||||
}
|
||||
|
||||
public String getReturnType() {
|
||||
return returnType;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Method getMethod() {
|
||||
return method;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Wraps a result for web from calling a native plugin.
|
||||
*/
|
||||
public class PluginResult {
|
||||
|
||||
private final JSObject json;
|
||||
|
||||
public PluginResult() {
|
||||
this(new JSObject());
|
||||
}
|
||||
|
||||
public PluginResult(JSObject json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
public PluginResult put(String name, boolean value) {
|
||||
return this.jsonPut(name, value);
|
||||
}
|
||||
|
||||
public PluginResult put(String name, double value) {
|
||||
return this.jsonPut(name, value);
|
||||
}
|
||||
|
||||
public PluginResult put(String name, int value) {
|
||||
return this.jsonPut(name, value);
|
||||
}
|
||||
|
||||
public PluginResult put(String name, long value) {
|
||||
return this.jsonPut(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a date as an ISO string
|
||||
*/
|
||||
public PluginResult put(String name, Date value) {
|
||||
TimeZone tz = TimeZone.getTimeZone("UTC");
|
||||
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
|
||||
df.setTimeZone(tz);
|
||||
return this.jsonPut(name, df.format(value));
|
||||
}
|
||||
|
||||
public PluginResult put(String name, Object value) {
|
||||
return this.jsonPut(name, value);
|
||||
}
|
||||
|
||||
public PluginResult put(String name, PluginResult value) {
|
||||
return this.jsonPut(name, value.json);
|
||||
}
|
||||
|
||||
PluginResult jsonPut(String name, Object value) {
|
||||
try {
|
||||
this.json.put(name, value);
|
||||
} catch (Exception ex) {
|
||||
Logger.error(Logger.tags("Plugin"), "", ex);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return this.json.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return plugin metadata and information about the result, if it succeeded the data, or error information if it didn't.
|
||||
* This is used for appRestoredResult, as it's technically a raw data response from a plugin.
|
||||
* @return the raw data response from the plugin.
|
||||
*/
|
||||
public JSObject getWrappedResult() {
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("pluginId", this.json.getString("pluginId"));
|
||||
ret.put("methodName", this.json.getString("methodName"));
|
||||
ret.put("success", this.json.getBoolean("success", false));
|
||||
ret.put("data", this.json.getJSObject("data"));
|
||||
ret.put("error", this.json.getJSObject("error"));
|
||||
return ret;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
/**
|
||||
* An data class used in conjunction with RouteProcessor.
|
||||
*
|
||||
* @see com.getcapacitor.RouteProcessor
|
||||
*/
|
||||
public class ProcessedRoute {
|
||||
|
||||
private String path;
|
||||
private boolean isAsset;
|
||||
private boolean ignoreAssetPath;
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public boolean isAsset() {
|
||||
return isAsset;
|
||||
}
|
||||
|
||||
public void setAsset(boolean asset) {
|
||||
isAsset = asset;
|
||||
}
|
||||
|
||||
public boolean isIgnoreAssetPath() {
|
||||
return ignoreAssetPath;
|
||||
}
|
||||
|
||||
public void setIgnoreAssetPath(boolean ignoreAssetPath) {
|
||||
this.ignoreAssetPath = ignoreAssetPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
/**
|
||||
* An interface used in the processing of routes
|
||||
*/
|
||||
public interface RouteProcessor {
|
||||
ProcessedRoute process(String basePath, String path);
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
public class ServerPath {
|
||||
|
||||
public enum PathType {
|
||||
BASE_PATH,
|
||||
ASSET_PATH
|
||||
}
|
||||
|
||||
private final PathType type;
|
||||
private final String path;
|
||||
|
||||
public ServerPath(PathType type, String path) {
|
||||
this.type = type;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public PathType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
}
|
180
capacitor-android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java
Executable file
180
capacitor-android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java
Executable file
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
* 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;
|
||||
package com.getcapacitor;
|
||||
|
||||
import android.net.Uri;
|
||||
import com.getcapacitor.util.HostMask;
|
||||
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<>();
|
||||
mText = null;
|
||||
}
|
||||
|
||||
private UriMatcher() {
|
||||
mCode = null;
|
||||
mWhich = -1;
|
||||
mChildren = new ArrayList<>();
|
||||
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.isEmpty() && 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 (i == -1 && token.contains("*")) {
|
||||
child.mWhich = MASK;
|
||||
} else 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 MASK:
|
||||
if (HostMask.Parser.parse(n.mText).matches(u)) {
|
||||
node = n;
|
||||
}
|
||||
break;
|
||||
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 static final int MASK = 3;
|
||||
|
||||
private Object mCode;
|
||||
private int mWhich;
|
||||
private String mText;
|
||||
private ArrayList<UriMatcher> mChildren;
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package com.getcapacitor;
|
||||
|
||||
import android.webkit.RenderProcessGoneDetail;
|
||||
import android.webkit.WebView;
|
||||
|
||||
/**
|
||||
* Provides callbacks associated with the {@link BridgeWebViewClient}
|
||||
*/
|
||||
public abstract class WebViewListener {
|
||||
|
||||
/**
|
||||
* Callback for page load event.
|
||||
*
|
||||
* @param webView The WebView that loaded
|
||||
*/
|
||||
public void onPageLoaded(WebView webView) {
|
||||
// Override me to add behavior to the page loaded event
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for onReceivedError event.
|
||||
*
|
||||
* @param webView The WebView that loaded
|
||||
*/
|
||||
public void onReceivedError(WebView webView) {
|
||||
// Override me to add behavior to handle the onReceivedError event
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for onReceivedHttpError event.
|
||||
*
|
||||
* @param webView The WebView that loaded
|
||||
*/
|
||||
public void onReceivedHttpError(WebView webView) {
|
||||
// Override me to add behavior to handle the onReceivedHttpError event
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for page start event.
|
||||
*
|
||||
* @param webView The WebView that loaded
|
||||
*/
|
||||
public void onPageStarted(WebView webView) {
|
||||
// Override me to add behavior to the page started event
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for render process gone event. Return true if the state is handled.
|
||||
*
|
||||
* @param webView The WebView that loaded
|
||||
* @return returns false by default if the listener is not overridden and used
|
||||
*/
|
||||
public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) {
|
||||
// Override me to add behavior to the web view render process gone event
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,749 @@
|
|||
/*
|
||||
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.getcapacitor;
|
||||
|
||||
import static com.getcapacitor.plugin.util.HttpRequestHandler.isDomainExcludedFromSSL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.util.Base64;
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebResourceRequest;
|
||||
import android.webkit.WebResourceResponse;
|
||||
import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
|
||||
import com.getcapacitor.plugin.util.HttpRequestHandler;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Helper class meant to be used with the android.webkit.WebView class to enable hosting assets,
|
||||
* resources and other data on 'virtual' https:// URL.
|
||||
* Hosting assets and resources on https:// 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 final String capacitorFileStart = Bridge.CAPACITOR_FILE_START;
|
||||
private static final String capacitorContentStart = Bridge.CAPACITOR_CONTENT_START;
|
||||
private String basePath;
|
||||
|
||||
private final UriMatcher uriMatcher;
|
||||
private final AndroidProtocolHandler protocolHandler;
|
||||
private final ArrayList<String> authorities;
|
||||
private boolean isAsset;
|
||||
// Whether to route all requests to paths without extensions back to `index.html`
|
||||
private final boolean html5mode;
|
||||
private final JSInjector jsInjector;
|
||||
private final Bridge bridge;
|
||||
|
||||
/**
|
||||
* 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<>();
|
||||
} else {
|
||||
tempResponseHeaders = responseHeaders;
|
||||
}
|
||||
tempResponseHeaders.put("Cache-Control", "no-cache");
|
||||
this.responseHeaders = tempResponseHeaders;
|
||||
}
|
||||
|
||||
public InputStream handle(WebResourceRequest request) {
|
||||
return handle(request.getUrl());
|
||||
}
|
||||
|
||||
public abstract 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;
|
||||
}
|
||||
}
|
||||
|
||||
WebViewLocalServer(Context context, Bridge bridge, JSInjector jsInjector, ArrayList<String> authorities, boolean html5mode) {
|
||||
uriMatcher = new UriMatcher(null);
|
||||
this.html5mode = html5mode;
|
||||
this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext());
|
||||
this.authorities = authorities;
|
||||
this.bridge = bridge;
|
||||
this.jsInjector = jsInjector;
|
||||
}
|
||||
|
||||
private static Uri parseAndVerifyUrl(String url) {
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
Uri uri = Uri.parse(url);
|
||||
if (uri == null) {
|
||||
Logger.error("Malformed URL: " + url);
|
||||
return null;
|
||||
}
|
||||
String path = uri.getPath();
|
||||
if (path == null || path.isEmpty()) {
|
||||
Logger.error("URL does not have a path: " + url);
|
||||
return null;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 request the request to process.
|
||||
* @return a response if the request URL had a matching handler, null if no handler was found.
|
||||
*/
|
||||
public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) {
|
||||
Uri loadingUrl = request.getUrl();
|
||||
|
||||
if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) {
|
||||
Logger.debug("Handling CapacitorHttp request: " + loadingUrl);
|
||||
try {
|
||||
return handleCapacitorHttpRequest(request);
|
||||
} catch (Exception e) {
|
||||
Logger.error(e.getLocalizedMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PathHandler handler;
|
||||
synchronized (uriMatcher) {
|
||||
handler = (PathHandler) uriMatcher.match(request.getUrl());
|
||||
}
|
||||
if (handler == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl(loadingUrl)) {
|
||||
Logger.debug("Handling local request: " + request.getUrl().toString());
|
||||
return handleLocalRequest(request, handler);
|
||||
} else {
|
||||
return handleProxyRequest(request, handler);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isLocalFile(Uri uri) {
|
||||
String path = uri.getPath();
|
||||
return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart);
|
||||
}
|
||||
|
||||
private boolean isErrorUrl(Uri uri) {
|
||||
String url = uri.toString();
|
||||
return url.equals(bridge.getErrorUrl());
|
||||
}
|
||||
|
||||
private boolean isMainUrl(Uri loadingUrl) {
|
||||
return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost()));
|
||||
}
|
||||
|
||||
private boolean isAllowedUrl(Uri loadingUrl) {
|
||||
return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost()));
|
||||
}
|
||||
|
||||
private String getReasonPhraseFromResponseCode(int code) {
|
||||
return switch (code) {
|
||||
case 100 -> "Continue";
|
||||
case 101 -> "Switching Protocols";
|
||||
case 200 -> "OK";
|
||||
case 201 -> "Created";
|
||||
case 202 -> "Accepted";
|
||||
case 203 -> "Non-Authoritative Information";
|
||||
case 204 -> "No Content";
|
||||
case 205 -> "Reset Content";
|
||||
case 206 -> "Partial Content";
|
||||
case 300 -> "Multiple Choices";
|
||||
case 301 -> "Moved Permanently";
|
||||
case 302 -> "Found";
|
||||
case 303 -> "See Other";
|
||||
case 304 -> "Not Modified";
|
||||
case 400 -> "Bad Request";
|
||||
case 401 -> "Unauthorized";
|
||||
case 403 -> "Forbidden";
|
||||
case 404 -> "Not Found";
|
||||
case 405 -> "Method Not Allowed";
|
||||
case 406 -> "Not Acceptable";
|
||||
case 407 -> "Proxy Authentication Required";
|
||||
case 408 -> "Request Timeout";
|
||||
case 409 -> "Conflict";
|
||||
case 410 -> "Gone";
|
||||
case 500 -> "Internal Server Error";
|
||||
case 501 -> "Not Implemented";
|
||||
case 502 -> "Bad Gateway";
|
||||
case 503 -> "Service Unavailable";
|
||||
case 504 -> "Gateway Timeout";
|
||||
case 505 -> "HTTP Version Not Supported";
|
||||
default -> "Unknown";
|
||||
};
|
||||
}
|
||||
|
||||
private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest request) throws IOException {
|
||||
String urlString = request.getUrl().getQueryParameter(Bridge.CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM);
|
||||
URL url = new URL(urlString);
|
||||
JSObject headers = new JSObject();
|
||||
|
||||
for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) {
|
||||
headers.put(header.getKey(), header.getValue());
|
||||
}
|
||||
|
||||
HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder()
|
||||
.setUrl(url)
|
||||
.setMethod(request.getMethod())
|
||||
.setHeaders(headers)
|
||||
.openConnection();
|
||||
|
||||
CapacitorHttpUrlConnection connection = connectionBuilder.build();
|
||||
|
||||
if (!isDomainExcludedFromSSL(bridge, url)) {
|
||||
connection.setSSLSocketFactory(bridge);
|
||||
}
|
||||
|
||||
connection.connect();
|
||||
|
||||
String mimeType = null;
|
||||
String encoding = null;
|
||||
Map<String, String> responseHeaders = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (String value : entry.getValue()) {
|
||||
builder.append(value);
|
||||
builder.append(", ");
|
||||
}
|
||||
builder.setLength(builder.length() - 2);
|
||||
|
||||
if ("Content-Type".equalsIgnoreCase(entry.getKey())) {
|
||||
String[] contentTypeParts = builder.toString().split(";");
|
||||
mimeType = contentTypeParts[0].trim();
|
||||
if (contentTypeParts.length > 1) {
|
||||
String[] encodingParts = contentTypeParts[1].split("=");
|
||||
if (encodingParts.length > 1) {
|
||||
encoding = encodingParts[1].trim();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
responseHeaders.put(entry.getKey(), builder.toString());
|
||||
}
|
||||
}
|
||||
|
||||
InputStream inputStream = connection.getErrorStream();
|
||||
if (inputStream == null) {
|
||||
inputStream = connection.getInputStream();
|
||||
}
|
||||
|
||||
if (null == mimeType) {
|
||||
mimeType = getMimeType(request.getUrl().getPath(), inputStream);
|
||||
}
|
||||
|
||||
int responseCode = connection.getResponseCode();
|
||||
String reasonPhrase = getReasonPhraseFromResponseCode(responseCode);
|
||||
|
||||
return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream);
|
||||
}
|
||||
|
||||
private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathHandler handler) {
|
||||
String path = request.getUrl().getPath();
|
||||
|
||||
if (request.getRequestHeaders().get("Range") != null) {
|
||||
InputStream responseStream = new LollipopLazyInputStream(handler, request);
|
||||
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 new WebResourceResponse(
|
||||
mimeType,
|
||||
handler.getEncoding(),
|
||||
statusCode,
|
||||
handler.getReasonPhrase(),
|
||||
tempResponseHeaders,
|
||||
responseStream
|
||||
);
|
||||
}
|
||||
|
||||
if (isLocalFile(request.getUrl()) || isErrorUrl(request.getUrl())) {
|
||||
InputStream responseStream = new LollipopLazyInputStream(handler, request);
|
||||
String mimeType = getMimeType(request.getUrl().getPath(), responseStream);
|
||||
int statusCode = getStatusCode(responseStream, handler.getStatusCode());
|
||||
return new WebResourceResponse(
|
||||
mimeType,
|
||||
handler.getEncoding(),
|
||||
statusCode,
|
||||
handler.getReasonPhrase(),
|
||||
handler.getResponseHeaders(),
|
||||
responseStream
|
||||
);
|
||||
}
|
||||
|
||||
if (path.equals("/cordova.js")) {
|
||||
return new WebResourceResponse(
|
||||
"application/javascript",
|
||||
handler.getEncoding(),
|
||||
handler.getStatusCode(),
|
||||
handler.getReasonPhrase(),
|
||||
handler.getResponseHeaders(),
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (path.equals("/") || (!request.getUrl().getLastPathSegment().contains(".") && html5mode)) {
|
||||
InputStream responseStream;
|
||||
try {
|
||||
String startPath = this.basePath + "/index.html";
|
||||
if (bridge.getRouteProcessor() != null) {
|
||||
ProcessedRoute processedRoute = bridge.getRouteProcessor().process(this.basePath, "/index.html");
|
||||
startPath = processedRoute.getPath();
|
||||
isAsset = processedRoute.isAsset();
|
||||
}
|
||||
|
||||
if (isAsset) {
|
||||
responseStream = protocolHandler.openAsset(startPath);
|
||||
} else {
|
||||
responseStream = protocolHandler.openFile(startPath);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.error("Unable to open index.html", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (jsInjector != null) {
|
||||
responseStream = jsInjector.getInjectedStream(responseStream);
|
||||
}
|
||||
|
||||
int statusCode = getStatusCode(responseStream, handler.getStatusCode());
|
||||
return new WebResourceResponse(
|
||||
"text/html",
|
||||
handler.getEncoding(),
|
||||
statusCode,
|
||||
handler.getReasonPhrase(),
|
||||
handler.getResponseHeaders(),
|
||||
responseStream
|
||||
);
|
||||
}
|
||||
|
||||
if ("/favicon.ico".equalsIgnoreCase(path)) {
|
||||
try {
|
||||
return new WebResourceResponse("image/png", null, null);
|
||||
} catch (Exception e) {
|
||||
Logger.error("favicon handling failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
int periodIndex = path.lastIndexOf(".");
|
||||
if (periodIndex >= 0) {
|
||||
String ext = path.substring(path.lastIndexOf("."));
|
||||
|
||||
InputStream responseStream = new LollipopLazyInputStream(handler, request);
|
||||
|
||||
// TODO: Conjure up a bit more subtlety than this
|
||||
if (ext.equals(".html") && jsInjector != null) {
|
||||
responseStream = jsInjector.getInjectedStream(responseStream);
|
||||
}
|
||||
|
||||
String mimeType = getMimeType(path, responseStream);
|
||||
int statusCode = getStatusCode(responseStream, handler.getStatusCode());
|
||||
return new WebResourceResponse(
|
||||
mimeType,
|
||||
handler.getEncoding(),
|
||||
statusCode,
|
||||
handler.getReasonPhrase(),
|
||||
handler.getResponseHeaders(),
|
||||
responseStream
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepends an {@code InputStream} with the JavaScript required by Capacitor.
|
||||
* This method only changes the original {@code InputStream} if {@code WebView} does not
|
||||
* support the {@code DOCUMENT_START_SCRIPT} feature.
|
||||
* @param original the original {@code InputStream}
|
||||
* @return the modified {@code InputStream}
|
||||
*/
|
||||
public InputStream getJavaScriptInjectedStream(InputStream original) {
|
||||
if (jsInjector != null) {
|
||||
return jsInjector.getInjectedStream(original);
|
||||
}
|
||||
return original;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instead of reading files from the filesystem/assets, proxy through to the URL
|
||||
* and let an external server handle it.
|
||||
* @param request
|
||||
* @param handler
|
||||
* @return
|
||||
*/
|
||||
private WebResourceResponse handleProxyRequest(WebResourceRequest request, PathHandler handler) {
|
||||
if (jsInjector != null) {
|
||||
final String method = request.getMethod();
|
||||
if (method.equals("GET")) {
|
||||
try {
|
||||
String url = request.getUrl().toString();
|
||||
Map<String, String> headers = request.getRequestHeaders();
|
||||
boolean isHtmlText = false;
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {
|
||||
isHtmlText = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (isHtmlText) {
|
||||
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
|
||||
for (Map.Entry<String, String> header : headers.entrySet()) {
|
||||
conn.setRequestProperty(header.getKey(), header.getValue());
|
||||
}
|
||||
String getCookie = CookieManager.getInstance().getCookie(url);
|
||||
if (getCookie != null) {
|
||||
conn.setRequestProperty("Cookie", getCookie);
|
||||
}
|
||||
conn.setRequestMethod(method);
|
||||
conn.setReadTimeout(30 * 1000);
|
||||
conn.setConnectTimeout(30 * 1000);
|
||||
if (request.getUrl().getUserInfo() != null) {
|
||||
byte[] userInfoBytes = request.getUrl().getUserInfo().getBytes(StandardCharsets.UTF_8);
|
||||
String base64 = Base64.encodeToString(userInfoBytes, Base64.NO_WRAP);
|
||||
conn.setRequestProperty("Authorization", "Basic " + base64);
|
||||
}
|
||||
|
||||
List<String> cookies = conn.getHeaderFields().get("Set-Cookie");
|
||||
if (cookies != null) {
|
||||
for (String cookie : cookies) {
|
||||
CookieManager.getInstance().setCookie(url, cookie);
|
||||
}
|
||||
}
|
||||
InputStream responseStream = conn.getInputStream();
|
||||
responseStream = jsInjector.getInjectedStream(responseStream);
|
||||
|
||||
return new WebResourceResponse(
|
||||
"text/html",
|
||||
handler.getEncoding(),
|
||||
handler.getStatusCode(),
|
||||
handler.getReasonPhrase(),
|
||||
handler.getResponseHeaders(),
|
||||
responseStream
|
||||
);
|
||||
}
|
||||
} 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")) {
|
||||
Logger.debug("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) {
|
||||
Logger.error("Unable to get mime type" + path, ex);
|
||||
}
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
private int getStatusCode(InputStream stream, int defaultCode) {
|
||||
int finalStatusCode = defaultCode;
|
||||
try {
|
||||
if (stream.available() == -1) {
|
||||
finalStatusCode = 404;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
finalStatusCode = 500;
|
||||
}
|
||||
return finalStatusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 https:// URL. Assets from the local path
|
||||
* <code>assetPath/...</code> will be available under
|
||||
* <code>https://{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").
|
||||
* @return prefixes under which the assets are hosted.
|
||||
*/
|
||||
public void hostAssets(String assetPath) {
|
||||
this.isAsset = true;
|
||||
this.basePath = assetPath;
|
||||
createHostingDetails();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hosts the application's files on an https:// URL. Files from the basePath
|
||||
* <code>basePath/...</code> will be available under
|
||||
* <code>https://{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").
|
||||
* @return prefixes under which the assets are hosted.
|
||||
*/
|
||||
public void hostFiles(final String basePath) {
|
||||
this.isAsset = false;
|
||||
this.basePath = basePath;
|
||||
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();
|
||||
|
||||
// Pass path to routeProcessor if present
|
||||
RouteProcessor routeProcessor = bridge.getRouteProcessor();
|
||||
boolean ignoreAssetPath = false;
|
||||
if (routeProcessor != null) {
|
||||
ProcessedRoute processedRoute = bridge.getRouteProcessor().process("", path);
|
||||
path = processedRoute.getPath();
|
||||
isAsset = processedRoute.isAsset();
|
||||
ignoreAssetPath = processedRoute.isIgnoreAssetPath();
|
||||
}
|
||||
|
||||
try {
|
||||
if (path.startsWith(capacitorContentStart)) {
|
||||
stream = protocolHandler.openContentUrl(url);
|
||||
} else if (path.startsWith(capacitorFileStart)) {
|
||||
stream = protocolHandler.openFile(path);
|
||||
} else if (!isAsset) {
|
||||
if (routeProcessor == null) {
|
||||
path = basePath + url.getPath();
|
||||
}
|
||||
|
||||
stream = protocolHandler.openFile(path);
|
||||
} else if (ignoreAssetPath) {
|
||||
stream = protocolHandler.openAsset(path);
|
||||
} else {
|
||||
stream = protocolHandler.openAsset(assetPath + path);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Logger.error("Unable to open asset URL: " + url);
|
||||
return null;
|
||||
}
|
||||
|
||||
return stream;
|
||||
}
|
||||
};
|
||||
|
||||
for (String authority : authorities) {
|
||||
registerUriForScheme(Bridge.CAPACITOR_HTTP_SCHEME, handler, authority);
|
||||
registerUriForScheme(Bridge.CAPACITOR_HTTPS_SCHEME, handler, authority);
|
||||
|
||||
String customScheme = this.bridge.getScheme();
|
||||
if (!customScheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !customScheme.equals(Bridge.CAPACITOR_HTTPS_SCHEME)) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* The KitKat WebView reads the InputStream on a separate threadpool. We can use that to
|
||||
* parallelize loading.
|
||||
*/
|
||||
private abstract static 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() : -1;
|
||||
}
|
||||
|
||||
@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 WebResourceRequest request;
|
||||
private InputStream is;
|
||||
|
||||
public LollipopLazyInputStream(PathHandler handler, WebResourceRequest request) {
|
||||
super(handler);
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InputStream handle() {
|
||||
return handler.handle(request);
|
||||
}
|
||||
}
|
||||
|
||||
public String getBasePath() {
|
||||
return this.basePath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.getcapacitor.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface ActivityCallback {
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.getcapacitor.annotation;
|
||||
|
||||
import android.content.Intent;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Base annotation for all Plugins
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CapacitorPlugin {
|
||||
/**
|
||||
* A custom name for the plugin, otherwise uses the
|
||||
* simple class name.
|
||||
*/
|
||||
String name() default "";
|
||||
|
||||
/**
|
||||
* Request codes this plugin uses and responds to, in order to tie
|
||||
* Android events back the plugin to handle.
|
||||
*
|
||||
* NOTE: This is a legacy option provided to support third party libraries
|
||||
* not currently implementing the new AndroidX Activity Results API. Plugins
|
||||
* without this limitation should use a registered callback with
|
||||
* {@link com.getcapacitor.Plugin#startActivityForResult(PluginCall, Intent, String)}
|
||||
*/
|
||||
int[] requestCodes() default {};
|
||||
|
||||
/**
|
||||
* Permissions this plugin needs, in order to make permission requests
|
||||
* easy if the plugin only needs basic permission prompting
|
||||
*/
|
||||
Permission[] permissions() default {};
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package com.getcapacitor.annotation;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
/**
|
||||
* Permission annotation for use with @CapacitorPlugin
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Permission {
|
||||
/**
|
||||
* An array of Android permission strings.
|
||||
* Eg: {Manifest.permission.ACCESS_COARSE_LOCATION}
|
||||
* or {"android.permission.ACCESS_COARSE_LOCATION"}
|
||||
*/
|
||||
String[] strings() default {};
|
||||
|
||||
/**
|
||||
* An optional name to use instead of the Android permission string.
|
||||
*/
|
||||
String alias() default "";
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package com.getcapacitor.annotation;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface PermissionCallback {
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package com.getcapacitor.cordova;
|
||||
|
||||
import android.webkit.CookieManager;
|
||||
import android.webkit.WebView;
|
||||
import org.apache.cordova.ICordovaCookieManager;
|
||||
|
||||
class CapacitorCordovaCookieManager implements ICordovaCookieManager {
|
||||
|
||||
protected final WebView webView;
|
||||
private final CookieManager cookieManager;
|
||||
|
||||
public CapacitorCordovaCookieManager(WebView webview) {
|
||||
webView = webview;
|
||||
cookieManager = CookieManager.getInstance();
|
||||
cookieManager.setAcceptThirdPartyCookies(webView, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCookiesEnabled(boolean accept) {
|
||||
cookieManager.setAcceptCookie(accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCookie(final String url, final String value) {
|
||||
cookieManager.setCookie(url, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCookie(final String url) {
|
||||
return cookieManager.getCookie(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCookies() {
|
||||
cookieManager.removeAllCookies(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
cookieManager.flush();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package com.getcapacitor.cordova;
|
||||
|
||||
import android.util.Pair;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import java.util.concurrent.Executors;
|
||||
import org.apache.cordova.CordovaInterfaceImpl;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
import org.json.JSONException;
|
||||
|
||||
public class MockCordovaInterfaceImpl extends CordovaInterfaceImpl {
|
||||
|
||||
public MockCordovaInterfaceImpl(AppCompatActivity activity) {
|
||||
super(activity, Executors.newCachedThreadPool());
|
||||
}
|
||||
|
||||
public CordovaPlugin getActivityResultCallback() {
|
||||
return this.activityResultCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks Cordova permission callbacks to handle permissions defined by a Cordova plugin.
|
||||
* Returns true if Cordova is handling the permission request with a registered code.
|
||||
*
|
||||
* @param requestCode
|
||||
* @param permissions
|
||||
* @param grantResults
|
||||
* @return true if Cordova handled the permission request, false if not
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public boolean handlePermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
|
||||
Pair<CordovaPlugin, Integer> callback = permissionResultCallbacks.getAndRemoveCallback(requestCode);
|
||||
if (callback != null) {
|
||||
callback.first.onRequestPermissionResult(callback.second, permissions, grantResults);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,284 @@
|
|||
package com.getcapacitor.cordova;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.view.View;
|
||||
import android.webkit.ValueCallback;
|
||||
import android.webkit.WebChromeClient;
|
||||
import android.webkit.WebView;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.ICordovaCookieManager;
|
||||
import org.apache.cordova.NativeToJsMessageQueue;
|
||||
import org.apache.cordova.PluginEntry;
|
||||
import org.apache.cordova.PluginManager;
|
||||
import org.apache.cordova.PluginResult;
|
||||
|
||||
public class MockCordovaWebViewImpl implements CordovaWebView {
|
||||
|
||||
private Context context;
|
||||
private PluginManager pluginManager;
|
||||
private CordovaPreferences preferences;
|
||||
private CordovaResourceApi resourceApi;
|
||||
private NativeToJsMessageQueue nativeToJsMessageQueue;
|
||||
private CordovaInterface cordova;
|
||||
private CapacitorCordovaCookieManager cookieManager;
|
||||
private WebView webView;
|
||||
private boolean hasPausedEver;
|
||||
|
||||
public MockCordovaWebViewImpl(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) {
|
||||
this.cordova = cordova;
|
||||
this.preferences = preferences;
|
||||
this.pluginManager = new PluginManager(this, this.cordova, pluginEntries);
|
||||
this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager);
|
||||
this.pluginManager.init();
|
||||
}
|
||||
|
||||
public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences, WebView webView) {
|
||||
this.cordova = cordova;
|
||||
this.webView = webView;
|
||||
this.preferences = preferences;
|
||||
this.pluginManager = new PluginManager(this, this.cordova, pluginEntries);
|
||||
this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager);
|
||||
nativeToJsMessageQueue = new NativeToJsMessageQueue();
|
||||
nativeToJsMessageQueue.addBridgeMode(new CapacitorEvalBridgeMode(webView, this.cordova));
|
||||
nativeToJsMessageQueue.setBridgeMode(0);
|
||||
this.cookieManager = new CapacitorCordovaCookieManager(webView);
|
||||
this.pluginManager.init();
|
||||
}
|
||||
|
||||
public static class CapacitorEvalBridgeMode extends NativeToJsMessageQueue.BridgeMode {
|
||||
|
||||
private final WebView webView;
|
||||
private final CordovaInterface cordova;
|
||||
|
||||
public CapacitorEvalBridgeMode(WebView webView, CordovaInterface cordova) {
|
||||
this.webView = webView;
|
||||
this.cordova = cordova;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) {
|
||||
cordova
|
||||
.getActivity()
|
||||
.runOnUiThread(
|
||||
() -> {
|
||||
String js = queue.popAndEncodeAsJs();
|
||||
if (js != null) {
|
||||
webView.evaluateJavascript(js, null);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return cordova != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView() {
|
||||
return this.webView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUrlIntoView(String url, boolean recreatePlugins) {
|
||||
if (url.equals("about:blank") || url.startsWith("javascript:")) {
|
||||
webView.loadUrl(url);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stopLoading() {}
|
||||
|
||||
@Override
|
||||
public boolean canGoBack() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearCache() {}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void clearCache(boolean b) {}
|
||||
|
||||
@Override
|
||||
public void clearHistory() {}
|
||||
|
||||
@Override
|
||||
public boolean backHistory() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handlePause(boolean keepRunning) {
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
hasPausedEver = true;
|
||||
pluginManager.onPause(keepRunning);
|
||||
triggerDocumentEvent("pause");
|
||||
// If app doesn't want to run in background
|
||||
if (!keepRunning) {
|
||||
// Pause JavaScript timers. This affects all webviews within the app!
|
||||
this.setPaused(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNewIntent(Intent intent) {
|
||||
if (this.pluginManager != null) {
|
||||
this.pluginManager.onNewIntent(intent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleResume(boolean keepRunning) {
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
this.setPaused(false);
|
||||
this.pluginManager.onResume(keepRunning);
|
||||
if (hasPausedEver) {
|
||||
triggerDocumentEvent("resume");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStart() {
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
pluginManager.onStart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleStop() {
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
pluginManager.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleDestroy() {
|
||||
if (!isInitialized()) {
|
||||
return;
|
||||
}
|
||||
this.pluginManager.onDestroy();
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void sendJavascript(String statememt) {
|
||||
nativeToJsMessageQueue.addJavaScript(statememt);
|
||||
}
|
||||
|
||||
public void eval(final String js, final ValueCallback<String> callback) {
|
||||
Handler mainHandler = new Handler(context.getMainLooper());
|
||||
mainHandler.post(() -> webView.evaluateJavascript(js, callback));
|
||||
}
|
||||
|
||||
public void triggerDocumentEvent(final String eventName) {
|
||||
eval("window.Capacitor.triggerEvent('" + eventName + "', 'document');", s -> {});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params) {}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean isCustomViewShowing() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {}
|
||||
|
||||
@Deprecated
|
||||
@Override
|
||||
public void hideCustomView() {}
|
||||
|
||||
@Override
|
||||
public CordovaResourceApi getResourceApi() {
|
||||
return this.resourceApi;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setButtonPlumbedToJs(int keyCode, boolean override) {}
|
||||
|
||||
@Override
|
||||
public boolean isButtonPlumbedToJs(int keyCode) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendPluginResult(PluginResult cr, String callbackId) {
|
||||
nativeToJsMessageQueue.addPluginResult(cr, callbackId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginManager getPluginManager() {
|
||||
return this.pluginManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CordovaWebViewEngine getEngine() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CordovaPreferences getPreferences() {
|
||||
return this.preferences;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ICordovaCookieManager getCookieManager() {
|
||||
return cookieManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUrl() {
|
||||
return webView.getUrl();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getContext() {
|
||||
return this.webView.getContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUrl(String url) {
|
||||
loadUrlIntoView(url, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postMessage(String id, Object data) {
|
||||
return pluginManager.postMessage(id, data);
|
||||
}
|
||||
|
||||
public void setPaused(boolean value) {
|
||||
if (value) {
|
||||
webView.onPause();
|
||||
webView.pauseTimers();
|
||||
} else {
|
||||
webView.onResume();
|
||||
webView.resumeTimers();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,239 @@
|
|||
package com.getcapacitor.plugin;
|
||||
|
||||
import com.getcapacitor.Bridge;
|
||||
import com.getcapacitor.Logger;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.CookieStore;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
public class CapacitorCookieManager extends CookieManager {
|
||||
|
||||
private final android.webkit.CookieManager webkitCookieManager;
|
||||
|
||||
private final String localUrl;
|
||||
|
||||
private final String serverUrl;
|
||||
|
||||
private final String TAG = "CapacitorCookies";
|
||||
|
||||
/**
|
||||
* Create a new cookie manager with the default cookie store and policy
|
||||
*/
|
||||
public CapacitorCookieManager(Bridge bridge) {
|
||||
this(null, null, bridge);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new cookie manager with specified cookie store and cookie policy.
|
||||
* @param store a {@code CookieStore} to be used by CookieManager. if {@code null}, cookie
|
||||
* manager will use a default one, which is an in-memory CookieStore implementation.
|
||||
* @param policy a {@code CookiePolicy} instance to be used by cookie manager as policy
|
||||
* callback. if {@code null}, ACCEPT_ORIGINAL_SERVER will be used.
|
||||
*/
|
||||
public CapacitorCookieManager(CookieStore store, CookiePolicy policy, Bridge bridge) {
|
||||
super(store, policy);
|
||||
webkitCookieManager = android.webkit.CookieManager.getInstance();
|
||||
this.localUrl = bridge.getLocalUrl();
|
||||
this.serverUrl = bridge.getServerUrl();
|
||||
}
|
||||
|
||||
public void removeSessionCookies() {
|
||||
this.webkitCookieManager.removeSessionCookies(null);
|
||||
}
|
||||
|
||||
public String getSanitizedDomain(String url) throws URISyntaxException {
|
||||
if (this.serverUrl != null && !this.serverUrl.isEmpty() && (url == null || url.isEmpty() || this.serverUrl.contains(url))) {
|
||||
url = this.serverUrl;
|
||||
} else if (this.localUrl != null && !this.localUrl.isEmpty() && (url == null || url.isEmpty() || this.localUrl.contains(url))) {
|
||||
url = this.localUrl;
|
||||
} else try {
|
||||
URI uri = new URI(url);
|
||||
String scheme = uri.getScheme();
|
||||
if (scheme == null || scheme.isEmpty()) {
|
||||
url = "https://" + url;
|
||||
}
|
||||
} catch (URISyntaxException e) {
|
||||
Logger.error(TAG, "Failed to get scheme from URL.", e);
|
||||
}
|
||||
|
||||
try {
|
||||
new URI(url);
|
||||
} catch (Exception error) {
|
||||
Logger.error(TAG, "Failed to get sanitized URL.", error);
|
||||
throw error;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
private String getDomainFromCookieString(String cookie) throws URISyntaxException {
|
||||
String[] domain = cookie.toLowerCase(Locale.ROOT).split("domain=");
|
||||
return getSanitizedDomain(domain.length <= 1 ? null : domain[1].split(";")[0].trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cookies for the given URL.
|
||||
* @param url the URL for which the cookies are requested
|
||||
* @return value the cookies as a string, using the format of the 'Cookie' HTTP request header
|
||||
*/
|
||||
public String getCookieString(String url) {
|
||||
try {
|
||||
url = getSanitizedDomain(url);
|
||||
Logger.info(TAG, "Getting cookies at: '" + url + "'");
|
||||
return webkitCookieManager.getCookie(url);
|
||||
} catch (Exception error) {
|
||||
Logger.error(TAG, "Failed to get cookies at the given URL.", error);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a cookie value for the given URL and key.
|
||||
* @param url the URL for which the cookies are requested
|
||||
* @param key the key of the cookie to search for
|
||||
* @return the {@code HttpCookie} value of the cookie at the key,
|
||||
* otherwise it will return a new empty {@code HttpCookie}
|
||||
*/
|
||||
public HttpCookie getCookie(String url, String key) {
|
||||
HttpCookie[] cookies = getCookies(url);
|
||||
for (HttpCookie cookie : cookies) {
|
||||
if (cookie.getName().equals(key)) {
|
||||
return cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an array of {@code HttpCookie} given a URL.
|
||||
* @param url the URL for which the cookies are requested
|
||||
* @return an {@code HttpCookie} array of non-expired cookies
|
||||
*/
|
||||
public HttpCookie[] getCookies(String url) {
|
||||
try {
|
||||
ArrayList<HttpCookie> cookieList = new ArrayList<>();
|
||||
String cookieString = getCookieString(url);
|
||||
if (cookieString != null) {
|
||||
String[] singleCookie = cookieString.split(";");
|
||||
for (String c : singleCookie) {
|
||||
HttpCookie parsed = HttpCookie.parse(c).get(0);
|
||||
parsed.setValue(parsed.getValue());
|
||||
cookieList.add(parsed);
|
||||
}
|
||||
}
|
||||
HttpCookie[] cookies = new HttpCookie[cookieList.size()];
|
||||
return cookieList.toArray(cookies);
|
||||
} catch (Exception ex) {
|
||||
return new HttpCookie[0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
|
||||
* be replaced with the new cookie. The cookie being set will be ignored if it is expired.
|
||||
* @param url the URL for which the cookie is to be set
|
||||
* @param value the cookie as a string, using the format of the 'Set-Cookie' HTTP response header
|
||||
*/
|
||||
public void setCookie(String url, String value) {
|
||||
try {
|
||||
url = getSanitizedDomain(url);
|
||||
Logger.info(TAG, "Setting cookie '" + value + "' at: '" + url + "'");
|
||||
webkitCookieManager.setCookie(url, value);
|
||||
flush();
|
||||
} catch (Exception error) {
|
||||
Logger.error(TAG, "Failed to set cookie.", error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a cookie for the given URL. Any existing cookie with the same host, path and name will
|
||||
* be replaced with the new cookie. The cookie being set will be ignored if it is expired.
|
||||
* @param url the URL for which the cookie is to be set
|
||||
* @param key the {@code HttpCookie} name to use for lookup
|
||||
* @param value the value of the {@code HttpCookie} given a key
|
||||
*/
|
||||
public void setCookie(String url, String key, String value) {
|
||||
String cookieValue = key + "=" + value;
|
||||
setCookie(url, cookieValue);
|
||||
}
|
||||
|
||||
public void setCookie(String url, String key, String value, String expires, String path) {
|
||||
String cookieValue = key + "=" + value + "; expires=" + expires + "; path=" + path;
|
||||
setCookie(url, cookieValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all cookies. This method is asynchronous.
|
||||
*/
|
||||
public void removeAllCookies() {
|
||||
webkitCookieManager.removeAllCookies(null);
|
||||
flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures all cookies currently accessible through the getCookie API are written to persistent
|
||||
* storage. This call will block the caller until it is done and may perform I/O.
|
||||
*/
|
||||
public void flush() {
|
||||
webkitCookieManager.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(URI uri, Map<String, List<String>> responseHeaders) {
|
||||
// make sure our args are valid
|
||||
if ((uri == null) || (responseHeaders == null)) return;
|
||||
|
||||
// go over the headers
|
||||
for (String headerKey : responseHeaders.keySet()) {
|
||||
// ignore headers which aren't cookie related
|
||||
if ((headerKey == null) || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue;
|
||||
|
||||
// process each of the headers
|
||||
for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) {
|
||||
try {
|
||||
// Set at the requested server url
|
||||
setCookie(uri.toString(), headerValue);
|
||||
|
||||
// Set at the defined domain in the response or at default capacitor hosted url
|
||||
setCookie(getDomainFromCookieString(headerValue), headerValue);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) {
|
||||
// make sure our args are valid
|
||||
if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException("Argument is null");
|
||||
|
||||
// save our url once
|
||||
String url = uri.toString();
|
||||
|
||||
// prepare our response
|
||||
Map<String, List<String>> res = new HashMap<>();
|
||||
|
||||
// get the cookie
|
||||
String cookie = getCookieString(url);
|
||||
|
||||
// return it
|
||||
if (cookie != null) res.put("Cookie", Collections.singletonList(cookie));
|
||||
return res;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CookieStore getCookieStore() {
|
||||
// we don't want anyone to work with this cookie store directly
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package com.getcapacitor.plugin;
|
||||
|
||||
import android.webkit.JavascriptInterface;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginConfig;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.CookieHandler;
|
||||
import java.net.HttpCookie;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@CapacitorPlugin
|
||||
public class CapacitorCookies extends Plugin {
|
||||
|
||||
CapacitorCookieManager cookieManager;
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
this.bridge.getWebView().addJavascriptInterface(this, "CapacitorCookiesAndroidInterface");
|
||||
this.cookieManager = new CapacitorCookieManager(null, java.net.CookiePolicy.ACCEPT_ALL, this.bridge);
|
||||
this.cookieManager.removeSessionCookies();
|
||||
CookieHandler.setDefault(this.cookieManager);
|
||||
super.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnDestroy() {
|
||||
super.handleOnDestroy();
|
||||
this.cookieManager.removeSessionCookies();
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean isEnabled() {
|
||||
PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies");
|
||||
return pluginConfig.getBoolean("enabled", false);
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public void setCookie(String domain, String action) {
|
||||
cookieManager.setCookie(domain, action);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getCookies(PluginCall call) {
|
||||
this.bridge.eval(
|
||||
"document.cookie",
|
||||
value -> {
|
||||
String cookies = value.substring(1, value.length() - 1);
|
||||
String[] cookieArray = cookies.split(";");
|
||||
|
||||
JSObject cookieMap = new JSObject();
|
||||
|
||||
for (String cookie : cookieArray) {
|
||||
if (cookie.length() > 0) {
|
||||
String[] keyValue = cookie.split("=", 2);
|
||||
|
||||
if (keyValue.length == 2) {
|
||||
String key = keyValue[0].trim();
|
||||
String val = keyValue[1].trim();
|
||||
try {
|
||||
key = URLDecoder.decode(keyValue[0].trim(), StandardCharsets.UTF_8.name());
|
||||
val = URLDecoder.decode(keyValue[1].trim(), StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException ignored) {}
|
||||
|
||||
cookieMap.put(key, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
call.resolve(cookieMap);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setCookie(PluginCall call) {
|
||||
String key = call.getString("key");
|
||||
if (null == key) {
|
||||
call.reject("Must provide key");
|
||||
}
|
||||
String value = call.getString("value");
|
||||
if (null == value) {
|
||||
call.reject("Must provide value");
|
||||
}
|
||||
String url = call.getString("url");
|
||||
String expires = call.getString("expires", "");
|
||||
String path = call.getString("path", "/");
|
||||
cookieManager.setCookie(url, key, value, expires, path);
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void deleteCookie(PluginCall call) {
|
||||
String key = call.getString("key");
|
||||
if (null == key) {
|
||||
call.reject("Must provide key");
|
||||
}
|
||||
String url = call.getString("url");
|
||||
cookieManager.setCookie(url, key + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT");
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void clearCookies(PluginCall call) {
|
||||
String url = call.getString("url");
|
||||
HttpCookie[] cookies = cookieManager.getCookies(url);
|
||||
for (HttpCookie cookie : cookies) {
|
||||
cookieManager.setCookie(url, cookie.getName() + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT");
|
||||
}
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void clearAllCookies(PluginCall call) {
|
||||
cookieManager.removeAllCookies();
|
||||
call.resolve();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package com.getcapacitor.plugin;
|
||||
|
||||
import android.Manifest;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginConfig;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
import com.getcapacitor.annotation.Permission;
|
||||
import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection;
|
||||
import com.getcapacitor.plugin.util.HttpRequestHandler;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@CapacitorPlugin(
|
||||
permissions = {
|
||||
@Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "HttpWrite"),
|
||||
@Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "HttpRead")
|
||||
}
|
||||
)
|
||||
public class CapacitorHttp extends Plugin {
|
||||
|
||||
private final Map<Runnable, PluginCall> activeRequests = new ConcurrentHashMap<>();
|
||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||
|
||||
@Override
|
||||
public void load() {
|
||||
this.bridge.getWebView().addJavascriptInterface(this, "CapacitorHttpAndroidInterface");
|
||||
super.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleOnDestroy() {
|
||||
super.handleOnDestroy();
|
||||
|
||||
for (Map.Entry<Runnable, PluginCall> entry : activeRequests.entrySet()) {
|
||||
Runnable job = entry.getKey();
|
||||
PluginCall call = entry.getValue();
|
||||
|
||||
if (call.getData().has("activeCapacitorHttpUrlConnection")) {
|
||||
try {
|
||||
CapacitorHttpUrlConnection connection = (CapacitorHttpUrlConnection) call
|
||||
.getData()
|
||||
.get("activeCapacitorHttpUrlConnection");
|
||||
connection.disconnect();
|
||||
call.getData().remove("activeCapacitorHttpUrlConnection");
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
getBridge().releaseCall(call);
|
||||
}
|
||||
|
||||
activeRequests.clear();
|
||||
executor.shutdownNow();
|
||||
}
|
||||
|
||||
private void http(final PluginCall call, final String httpMethod) {
|
||||
Runnable asyncHttpCall = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge());
|
||||
call.resolve(response);
|
||||
} catch (Exception e) {
|
||||
call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e);
|
||||
} finally {
|
||||
activeRequests.remove(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (!executor.isShutdown()) {
|
||||
activeRequests.put(asyncHttpCall, call);
|
||||
executor.submit(asyncHttpCall);
|
||||
} else {
|
||||
call.reject("Failed to execute request - Http Plugin was shutdown");
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
public boolean isEnabled() {
|
||||
PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorHttp");
|
||||
return pluginConfig.getBoolean("enabled", false);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void request(final PluginCall call) {
|
||||
this.http(call, null);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void get(final PluginCall call) {
|
||||
this.http(call, "GET");
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void post(final PluginCall call) {
|
||||
this.http(call, "POST");
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void put(final PluginCall call) {
|
||||
this.http(call, "PUT");
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void patch(final PluginCall call) {
|
||||
this.http(call, "PATCH");
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void delete(final PluginCall call) {
|
||||
this.http(call, "DELETE");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package com.getcapacitor.plugin;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.SharedPreferences;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
@CapacitorPlugin
|
||||
public class WebView extends Plugin {
|
||||
|
||||
public static final String WEBVIEW_PREFS_NAME = "CapWebViewSettings";
|
||||
public static final String CAP_SERVER_PATH = "serverBasePath";
|
||||
|
||||
@PluginMethod
|
||||
public void setServerAssetPath(PluginCall call) {
|
||||
String path = call.getString("path");
|
||||
bridge.setServerAssetPath(path);
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void setServerBasePath(PluginCall call) {
|
||||
String path = call.getString("path");
|
||||
bridge.setServerBasePath(path);
|
||||
call.resolve();
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void getServerBasePath(PluginCall call) {
|
||||
String path = bridge.getServerBasePath();
|
||||
JSObject ret = new JSObject();
|
||||
ret.put("path", path);
|
||||
call.resolve(ret);
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void persistServerBasePath(PluginCall call) {
|
||||
String path = bridge.getServerBasePath();
|
||||
SharedPreferences prefs = getContext().getSharedPreferences(WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putString(CAP_SERVER_PATH, path);
|
||||
editor.apply();
|
||||
call.resolve();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
package com.getcapacitor.plugin.util;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.res.AssetManager;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.StrictMode;
|
||||
import androidx.core.content.FileProvider;
|
||||
import com.getcapacitor.Logger;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manager for assets.
|
||||
*/
|
||||
public final class AssetUtil {
|
||||
|
||||
public static final int RESOURCE_ID_ZERO_VALUE = 0;
|
||||
// Name of the storage folder
|
||||
private static final String STORAGE_FOLDER = "/capacitorassets";
|
||||
|
||||
// Ref to the context passed through the constructor to access the
|
||||
// resources and app directory.
|
||||
private final Context context;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context Application context.
|
||||
*/
|
||||
private AssetUtil(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static method to retrieve class instance.
|
||||
*
|
||||
* @param context Application context.
|
||||
*/
|
||||
public static AssetUtil getInstance(Context context) {
|
||||
return new AssetUtil(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URI for a path.
|
||||
*
|
||||
* @param path The given path.
|
||||
*/
|
||||
public Uri parse(String path) {
|
||||
if (path == null || path.isEmpty()) {
|
||||
return Uri.EMPTY;
|
||||
} else if (path.startsWith("res:")) {
|
||||
return getUriForResourcePath(path);
|
||||
} else if (path.startsWith("file:///")) {
|
||||
return getUriFromPath(path);
|
||||
} else if (path.startsWith("file://")) {
|
||||
return getUriFromAsset(path);
|
||||
} else if (path.startsWith("http")) {
|
||||
return getUriFromRemote(path);
|
||||
} else if (path.startsWith("content://")) {
|
||||
return Uri.parse(path);
|
||||
}
|
||||
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* URI for a file.
|
||||
*
|
||||
* @param path Absolute path like file:///...
|
||||
*
|
||||
* @return URI pointing to the given path.
|
||||
*/
|
||||
private Uri getUriFromPath(String path) {
|
||||
String absPath = path.replaceFirst("file://", "").replaceFirst("\\?.*$", "");
|
||||
File file = new File(absPath);
|
||||
|
||||
if (!file.exists()) {
|
||||
Logger.error("File not found: " + file.getAbsolutePath());
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
|
||||
return getUriFromFile(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* URI for an asset.
|
||||
*
|
||||
* @param path Asset path like file://...
|
||||
*
|
||||
* @return URI pointing to the given path.
|
||||
*/
|
||||
private Uri getUriFromAsset(String path) {
|
||||
String resPath = path.replaceFirst("file:/", "www").replaceFirst("\\?.*$", "");
|
||||
String fileName = resPath.substring(resPath.lastIndexOf('/') + 1);
|
||||
File file = getTmpFile(fileName);
|
||||
|
||||
if (file == null) return Uri.EMPTY;
|
||||
|
||||
try {
|
||||
AssetManager assets = context.getAssets();
|
||||
InputStream in = assets.open(resPath);
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
copyFile(in, out);
|
||||
} catch (Exception e) {
|
||||
Logger.error("File not found: assets/" + resPath);
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
|
||||
return getUriFromFile(file);
|
||||
}
|
||||
|
||||
/**
|
||||
* The URI for a resource.
|
||||
*
|
||||
* @param path The given relative path.
|
||||
*
|
||||
* @return URI pointing to the given path.
|
||||
*/
|
||||
private Uri getUriForResourcePath(String path) {
|
||||
Resources res = context.getResources();
|
||||
String resPath = path.replaceFirst("res://", "");
|
||||
int resId = getResId(resPath);
|
||||
|
||||
if (resId == 0) {
|
||||
Logger.error("File not found: " + resPath);
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
|
||||
return new Uri.Builder()
|
||||
.scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
|
||||
.authority(res.getResourcePackageName(resId))
|
||||
.appendPath(res.getResourceTypeName(resId))
|
||||
.appendPath(res.getResourceEntryName(resId))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uri from remote located content.
|
||||
*
|
||||
* @param path Remote address.
|
||||
*
|
||||
* @return Uri of the downloaded file.
|
||||
*/
|
||||
private Uri getUriFromRemote(String path) {
|
||||
File file = getTmpFile();
|
||||
|
||||
if (file == null) return Uri.EMPTY;
|
||||
|
||||
try {
|
||||
URL url = new URL(path);
|
||||
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
|
||||
|
||||
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
|
||||
|
||||
StrictMode.setThreadPolicy(policy);
|
||||
|
||||
connection.setRequestProperty("Connection", "close");
|
||||
connection.setConnectTimeout(5000);
|
||||
connection.connect();
|
||||
|
||||
InputStream in = connection.getInputStream();
|
||||
FileOutputStream out = new FileOutputStream(file);
|
||||
|
||||
copyFile(in, out);
|
||||
return getUriFromFile(file);
|
||||
} catch (MalformedURLException e) {
|
||||
Logger.error(Logger.tags("Asset"), "Incorrect URL", e);
|
||||
} catch (FileNotFoundException e) {
|
||||
Logger.error(Logger.tags("Asset"), "Failed to create new File from HTTP Content", e);
|
||||
} catch (IOException e) {
|
||||
Logger.error(Logger.tags("Asset"), "No Input can be created from http Stream", e);
|
||||
}
|
||||
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy content from input stream into output stream.
|
||||
*
|
||||
* @param in The input stream.
|
||||
* @param out The output stream.
|
||||
*/
|
||||
private void copyFile(InputStream in, FileOutputStream out) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int read;
|
||||
|
||||
try {
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
out.flush();
|
||||
out.close();
|
||||
} catch (Exception e) {
|
||||
Logger.error("Error copying", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resource ID for drawable.
|
||||
*
|
||||
* @param resPath Resource path as string.
|
||||
*
|
||||
* @return The resource ID or 0 if not found.
|
||||
*/
|
||||
public int getResId(String resPath) {
|
||||
int resId = getResId(context.getResources(), resPath);
|
||||
|
||||
if (resId == 0) {
|
||||
resId = getResId(Resources.getSystem(), resPath);
|
||||
}
|
||||
|
||||
return resId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get resource ID.
|
||||
*
|
||||
* @param res The resources where to look for.
|
||||
* @param resPath The name of the resource.
|
||||
*
|
||||
* @return The resource ID or 0 if not found.
|
||||
*/
|
||||
private int getResId(Resources res, String resPath) {
|
||||
String pkgName = getPkgName(res);
|
||||
String resName = getBaseName(resPath);
|
||||
int resId;
|
||||
|
||||
resId = res.getIdentifier(resName, "mipmap", pkgName);
|
||||
|
||||
if (resId == 0) {
|
||||
resId = res.getIdentifier(resName, "drawable", pkgName);
|
||||
}
|
||||
|
||||
if (resId == 0) {
|
||||
resId = res.getIdentifier(resName, "raw", pkgName);
|
||||
}
|
||||
|
||||
return resId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert URI to Bitmap.
|
||||
*
|
||||
* @param uri Internal image URI
|
||||
*/
|
||||
public Bitmap getIconFromUri(Uri uri) throws IOException {
|
||||
InputStream input = context.getContentResolver().openInputStream(uri);
|
||||
return BitmapFactory.decodeStream(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract name of drawable resource from path.
|
||||
*
|
||||
* @param resPath Resource path as string.
|
||||
*/
|
||||
private String getBaseName(String resPath) {
|
||||
String drawable = resPath;
|
||||
|
||||
if (drawable.contains("/")) {
|
||||
drawable = drawable.substring(drawable.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
if (resPath.contains(".")) {
|
||||
drawable = drawable.substring(0, drawable.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
return drawable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file located under the external cache dir of that app.
|
||||
*
|
||||
* @return File with a random UUID name.
|
||||
*/
|
||||
private File getTmpFile() {
|
||||
return getTmpFile(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a file located under the external cache dir of that app.
|
||||
*
|
||||
* @param name The name of the file.
|
||||
*
|
||||
* @return File with the provided name.
|
||||
*/
|
||||
private File getTmpFile(String name) {
|
||||
File dir = context.getExternalCacheDir();
|
||||
|
||||
if (dir == null) {
|
||||
dir = context.getCacheDir();
|
||||
}
|
||||
|
||||
if (dir == null) {
|
||||
Logger.error(Logger.tags("Asset"), "Missing cache dir", null);
|
||||
return null;
|
||||
}
|
||||
|
||||
String storage = dir.toString() + STORAGE_FOLDER;
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
new File(storage).mkdir();
|
||||
|
||||
return new File(storage, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get content URI for the specified file.
|
||||
*
|
||||
* @param file The file to get the URI.
|
||||
*
|
||||
* @return content://...
|
||||
*/
|
||||
private Uri getUriFromFile(File file) {
|
||||
try {
|
||||
String authority = context.getPackageName() + ".provider";
|
||||
return FileProvider.getUriForFile(context, authority, file);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Logger.error("File not supported by provider", e);
|
||||
return Uri.EMPTY;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Package name specified by the resource bundle.
|
||||
*/
|
||||
private String getPkgName(Resources res) {
|
||||
return res == Resources.getSystem() ? "android" : context.getPackageName();
|
||||
}
|
||||
|
||||
public static int getResourceID(Context context, String resourceName, String dir) {
|
||||
return context.getResources().getIdentifier(resourceName, dir, context.getPackageName());
|
||||
}
|
||||
|
||||
public static String getResourceBaseName(String resPath) {
|
||||
if (resPath == null) return null;
|
||||
|
||||
if (resPath.contains("/")) {
|
||||
return resPath.substring(resPath.lastIndexOf('/') + 1);
|
||||
}
|
||||
|
||||
if (resPath.contains(".")) {
|
||||
return resPath.substring(0, resPath.lastIndexOf('.'));
|
||||
}
|
||||
|
||||
return resPath;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,478 @@
|
|||
package com.getcapacitor.plugin.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.LocaleList;
|
||||
import android.text.TextUtils;
|
||||
import com.getcapacitor.Bridge;
|
||||
import com.getcapacitor.JSArray;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.JSValue;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.UnknownServiceException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection {
|
||||
|
||||
private final HttpURLConnection connection;
|
||||
|
||||
/**
|
||||
* Make a new CapacitorHttpUrlConnection instance, which wraps around HttpUrlConnection
|
||||
* and provides some helper functions for setting request headers and the request body
|
||||
* @param conn the base HttpUrlConnection. You can pass the value from
|
||||
* {@code (HttpUrlConnection) URL.openConnection()}
|
||||
*/
|
||||
public CapacitorHttpUrlConnection(HttpURLConnection conn) {
|
||||
connection = conn;
|
||||
this.setDefaultRequestProperties();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying HttpUrlConnection value
|
||||
* @return the underlying HttpUrlConnection value
|
||||
*/
|
||||
public HttpURLConnection getHttpConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public void disconnect() {
|
||||
connection.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value of the {@code allowUserInteraction} field of
|
||||
* this {@code URLConnection}.
|
||||
*
|
||||
* @param isAllowedInteraction the new value.
|
||||
* @throws IllegalStateException if already connected
|
||||
*/
|
||||
public void setAllowUserInteraction(boolean isAllowedInteraction) {
|
||||
connection.setAllowUserInteraction(isAllowedInteraction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the method for the URL request, one of:
|
||||
* <UL>
|
||||
* <LI>GET
|
||||
* <LI>POST
|
||||
* <LI>HEAD
|
||||
* <LI>OPTIONS
|
||||
* <LI>PUT
|
||||
* <LI>DELETE
|
||||
* <LI>TRACE
|
||||
* </UL> are legal, subject to protocol restrictions. The default
|
||||
* method is GET.
|
||||
*
|
||||
* @param method the HTTP method
|
||||
* @exception ProtocolException if the method cannot be reset or if
|
||||
* the requested method isn't valid for HTTP.
|
||||
* @exception SecurityException if a security manager is set and the
|
||||
* method is "TRACE", but the "allowHttpTrace"
|
||||
* NetPermission is not granted.
|
||||
*/
|
||||
public void setRequestMethod(String method) throws ProtocolException {
|
||||
connection.setRequestMethod(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a specified timeout value, in milliseconds, to be used
|
||||
* when opening a communications link to the resource referenced
|
||||
* by this URLConnection. If the timeout expires before the
|
||||
* connection can be established, a
|
||||
* java.net.SocketTimeoutException is raised. A timeout of zero is
|
||||
* interpreted as an infinite timeout.
|
||||
*
|
||||
* <p><strong>Warning</strong>: If the hostname resolves to multiple IP
|
||||
* addresses, Android's default implementation of {@link HttpURLConnection}
|
||||
* will try each in
|
||||
* <a href="http://www.ietf.org/rfc/rfc3484.txt">RFC 3484</a> order. If
|
||||
* connecting to each of these addresses fails, multiple timeouts will
|
||||
* elapse before the connect attempt throws an exception. Host names
|
||||
* that support both IPv6 and IPv4 always have at least 2 IP addresses.
|
||||
*
|
||||
* @param timeout an {@code int} that specifies the connect
|
||||
* timeout value in milliseconds
|
||||
* @throws IllegalArgumentException if the timeout parameter is negative
|
||||
*/
|
||||
public void setConnectTimeout(int timeout) {
|
||||
if (timeout < 0) {
|
||||
throw new IllegalArgumentException("timeout can not be negative");
|
||||
}
|
||||
connection.setConnectTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read timeout to a specified timeout, in
|
||||
* milliseconds. A non-zero value specifies the timeout when
|
||||
* reading from Input stream when a connection is established to a
|
||||
* resource. If the timeout expires before there is data available
|
||||
* for read, a java.net.SocketTimeoutException is raised. A
|
||||
* timeout of zero is interpreted as an infinite timeout.
|
||||
*
|
||||
* @param timeout an {@code int} that specifies the timeout
|
||||
* value to be used in milliseconds
|
||||
* @throws IllegalArgumentException if the timeout parameter is negative
|
||||
*/
|
||||
public void setReadTimeout(int timeout) {
|
||||
if (timeout < 0) {
|
||||
throw new IllegalArgumentException("timeout can not be negative");
|
||||
}
|
||||
connection.setReadTimeout(timeout);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether automatic HTTP redirects should be disabled
|
||||
* @param disableRedirects the flag to determine if redirects should be followed
|
||||
*/
|
||||
public void setDisableRedirects(boolean disableRedirects) {
|
||||
connection.setInstanceFollowRedirects(!disableRedirects);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request headers given a JSObject of key-value pairs
|
||||
* @param headers the JSObject values to map to the HttpUrlConnection request headers
|
||||
*/
|
||||
public void setRequestHeaders(JSObject headers) {
|
||||
Iterator<String> keys = headers.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
String value = headers.getString(key);
|
||||
connection.setRequestProperty(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of the {@code doOutput} field for this
|
||||
* {@code URLConnection} to the specified value.
|
||||
* <p>
|
||||
* A URL connection can be used for input and/or output. Set the DoOutput
|
||||
* flag to true if you intend to use the URL connection for output,
|
||||
* false if not. The default is false.
|
||||
*
|
||||
* @param shouldDoOutput the new value.
|
||||
* @throws IllegalStateException if already connected
|
||||
*/
|
||||
public void setDoOutput(boolean shouldDoOutput) {
|
||||
connection.setDoOutput(shouldDoOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param call
|
||||
* @throws JSONException
|
||||
* @throws IOException
|
||||
*/
|
||||
public void setRequestBody(PluginCall call, JSValue body) throws JSONException, IOException {
|
||||
setRequestBody(call, body, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param call
|
||||
* @throws JSONException
|
||||
* @throws IOException
|
||||
*/
|
||||
public void setRequestBody(PluginCall call, JSValue body, String bodyType) throws JSONException, IOException {
|
||||
String contentType = connection.getRequestProperty("Content-Type");
|
||||
String dataString = "";
|
||||
|
||||
if (contentType == null || contentType.isEmpty()) return;
|
||||
|
||||
if (contentType.contains("application/json")) {
|
||||
JSArray jsArray = null;
|
||||
if (body != null) {
|
||||
dataString = body.toString();
|
||||
} else {
|
||||
jsArray = call.getArray("data", null);
|
||||
}
|
||||
if (jsArray != null) {
|
||||
dataString = jsArray.toString();
|
||||
} else if (body == null) {
|
||||
dataString = call.getString("data");
|
||||
}
|
||||
this.writeRequestBody(dataString != null ? dataString : "");
|
||||
} else if (bodyType != null && bodyType.equals("file")) {
|
||||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
os.write(Base64.getDecoder().decode(body.toString()));
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
} else if (contentType.contains("application/x-www-form-urlencoded")) {
|
||||
try {
|
||||
JSObject obj = body.toJSObject();
|
||||
this.writeObjectRequestBody(obj);
|
||||
} catch (Exception e) {
|
||||
// Body is not a valid JSON, treat it as an already formatted string
|
||||
this.writeRequestBody(body.toString());
|
||||
}
|
||||
} else if (bodyType != null && bodyType.equals("formData")) {
|
||||
this.writeFormDataRequestBody(contentType, body.toJSArray());
|
||||
} else {
|
||||
this.writeRequestBody(body.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the provided string to the HTTP connection managed by this instance.
|
||||
*
|
||||
* @param body The string value to write to the connection stream.
|
||||
*/
|
||||
private void writeRequestBody(String body) throws IOException {
|
||||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
|
||||
os.write(body.getBytes(StandardCharsets.UTF_8));
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeObjectRequestBody(JSObject object) throws IOException, JSONException {
|
||||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
|
||||
Iterator<String> keys = object.keys();
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
Object d = object.get(key);
|
||||
os.writeBytes(key);
|
||||
os.writeBytes("=");
|
||||
os.writeBytes(URLEncoder.encode(d.toString(), "UTF-8"));
|
||||
|
||||
if (keys.hasNext()) {
|
||||
os.writeBytes("&");
|
||||
}
|
||||
}
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException {
|
||||
try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) {
|
||||
String boundary = contentType.split(";")[1].split("=")[1];
|
||||
String lineEnd = "\r\n";
|
||||
String twoHyphens = "--";
|
||||
|
||||
for (Object e : entries.toList()) {
|
||||
if (e instanceof JSONObject) {
|
||||
JSONObject entry = (JSONObject) e;
|
||||
String type = entry.getString("type");
|
||||
String key = entry.getString("key");
|
||||
String value = entry.getString("value");
|
||||
if (type.equals("string")) {
|
||||
os.writeBytes(twoHyphens + boundary + lineEnd);
|
||||
os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"" + lineEnd + lineEnd);
|
||||
os.write(value.getBytes(StandardCharsets.UTF_8));
|
||||
os.writeBytes(lineEnd);
|
||||
} else if (type.equals("base64File")) {
|
||||
String fileName = entry.getString("fileName");
|
||||
String fileContentType = entry.getString("contentType");
|
||||
|
||||
os.writeBytes(twoHyphens + boundary + lineEnd);
|
||||
os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + fileName + "\"" + lineEnd);
|
||||
os.writeBytes("Content-Type: " + fileContentType + lineEnd);
|
||||
os.writeBytes("Content-Transfer-Encoding: binary" + lineEnd);
|
||||
os.writeBytes(lineEnd);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
os.write(Base64.getDecoder().decode(value));
|
||||
} else {
|
||||
os.write(android.util.Base64.decode(value, android.util.Base64.DEFAULT));
|
||||
}
|
||||
|
||||
os.writeBytes(lineEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
os.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
|
||||
os.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a communications link to the resource referenced by this
|
||||
* URL, if such a connection has not already been established.
|
||||
* <p>
|
||||
* If the {@code connect} method is called when the connection
|
||||
* has already been opened (indicated by the {@code connected}
|
||||
* field having the value {@code true}), the call is ignored.
|
||||
* <p>
|
||||
* URLConnection objects go through two phases: first they are
|
||||
* created, then they are connected. After being created, and
|
||||
* before being connected, various options can be specified
|
||||
* (e.g., doInput and UseCaches). After connecting, it is an
|
||||
* error to try to set them. Operations that depend on being
|
||||
* connected, like getContentLength, will implicitly perform the
|
||||
* connection, if necessary.
|
||||
*
|
||||
* @throws SocketTimeoutException if the timeout expires before
|
||||
* the connection can be established
|
||||
* @exception IOException if an I/O error occurs while opening the
|
||||
* connection.
|
||||
*/
|
||||
public void connect() throws IOException {
|
||||
connection.connect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the status code from an HTTP response message.
|
||||
* For example, in the case of the following status lines:
|
||||
* <PRE>
|
||||
* HTTP/1.0 200 OK
|
||||
* HTTP/1.0 401 Unauthorized
|
||||
* </PRE>
|
||||
* It will return 200 and 401 respectively.
|
||||
* Returns -1 if no code can be discerned
|
||||
* from the response (i.e., the response is not valid HTTP).
|
||||
* @throws IOException if an error occurred connecting to the server.
|
||||
* @return the HTTP Status-Code, or -1
|
||||
*/
|
||||
public int getResponseCode() throws IOException {
|
||||
return connection.getResponseCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of this {@code URLConnection}'s {@code URL}
|
||||
* field.
|
||||
*
|
||||
* @return the value of this {@code URLConnection}'s {@code URL}
|
||||
* field.
|
||||
*/
|
||||
public URL getURL() {
|
||||
return connection.getURL();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error stream if the connection failed
|
||||
* but the server sent useful data nonetheless. The
|
||||
* typical example is when an HTTP server responds
|
||||
* with a 404, which will cause a FileNotFoundException
|
||||
* to be thrown in connect, but the server sent an HTML
|
||||
* help page with suggestions as to what to do.
|
||||
*
|
||||
* <p>This method will not cause a connection to be initiated. If
|
||||
* the connection was not connected, or if the server did not have
|
||||
* an error while connecting or if the server had an error but
|
||||
* no error data was sent, this method will return null. This is
|
||||
* the default.
|
||||
*
|
||||
* @return an error stream if any, null if there have been no
|
||||
* errors, the connection is not connected or the server sent no
|
||||
* useful data.
|
||||
*/
|
||||
@Override
|
||||
public InputStream getErrorStream() {
|
||||
return connection.getErrorStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the named header field.
|
||||
* <p>
|
||||
* If called on a connection that sets the same header multiple times
|
||||
* with possibly different values, only the last value is returned.
|
||||
*
|
||||
*
|
||||
* @param name the name of a header field.
|
||||
* @return the value of the named header field, or {@code null}
|
||||
* if there is no such field in the header.
|
||||
*/
|
||||
@Override
|
||||
public String getHeaderField(String name) {
|
||||
return connection.getHeaderField(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an input stream that reads from this open connection.
|
||||
*
|
||||
* A SocketTimeoutException can be thrown when reading from the
|
||||
* returned input stream if the read timeout expires before data
|
||||
* is available for read.
|
||||
*
|
||||
* @return an input stream that reads from this open connection.
|
||||
* @exception IOException if an I/O error occurs while
|
||||
* creating the input stream.
|
||||
* @exception UnknownServiceException if the protocol does not support
|
||||
* input.
|
||||
* @see #setReadTimeout(int)
|
||||
*/
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
return connection.getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable Map of the header fields.
|
||||
* The Map keys are Strings that represent the
|
||||
* response-header field names. Each Map value is an
|
||||
* unmodifiable List of Strings that represents
|
||||
* the corresponding field values.
|
||||
*
|
||||
* @return a Map of header fields
|
||||
*/
|
||||
public Map<String, List<String>> getHeaderFields() {
|
||||
return connection.getHeaderFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default request properties on the newly created connection.
|
||||
* This is called as early as possible to allow overrides by user-provided values.
|
||||
*/
|
||||
private void setDefaultRequestProperties() {
|
||||
String acceptLanguage = buildDefaultAcceptLanguageProperty();
|
||||
if (!TextUtils.isEmpty(acceptLanguage)) {
|
||||
connection.setRequestProperty("Accept-Language", acceptLanguage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns a locale string describing the device's current locale preferences.
|
||||
*/
|
||||
private String buildDefaultAcceptLanguageProperty() {
|
||||
Locale locale;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
locale = LocaleList.getDefault().get(0);
|
||||
} else {
|
||||
locale = Locale.getDefault();
|
||||
}
|
||||
String result = "";
|
||||
String lang = locale.getLanguage();
|
||||
String country = locale.getCountry();
|
||||
if (!TextUtils.isEmpty(lang)) {
|
||||
if (!TextUtils.isEmpty(country)) {
|
||||
result = String.format("%s-%s,%s;q=0.5", lang, country, lang);
|
||||
} else {
|
||||
result = String.format("%s;q=0.5", lang);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void setSSLSocketFactory(Bridge bridge) {
|
||||
// Attach SSL Certificates if Enterprise Plugin is available
|
||||
try {
|
||||
Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
|
||||
Method method = sslPinningImpl.getDeclaredMethod("getSSLSocketFactory", Bridge.class);
|
||||
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) method.invoke(
|
||||
sslPinningImpl.getDeclaredConstructor().newInstance(),
|
||||
bridge
|
||||
);
|
||||
if (sslSocketFactory != null) {
|
||||
((HttpsURLConnection) this.connection).setSSLSocketFactory(sslSocketFactory);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,452 @@
|
|||
package com.getcapacitor.plugin.util;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
import com.getcapacitor.Bridge;
|
||||
import com.getcapacitor.JSArray;
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.JSValue;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class HttpRequestHandler {
|
||||
|
||||
/**
|
||||
* An enum specifying conventional HTTP Response Types
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
|
||||
*/
|
||||
public enum ResponseType {
|
||||
ARRAY_BUFFER("arraybuffer"),
|
||||
BLOB("blob"),
|
||||
DOCUMENT("document"),
|
||||
JSON("json"),
|
||||
TEXT("text");
|
||||
|
||||
private final String name;
|
||||
|
||||
ResponseType(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
static final ResponseType DEFAULT = TEXT;
|
||||
|
||||
public static ResponseType parse(String value) {
|
||||
for (ResponseType responseType : values()) {
|
||||
if (responseType.name.equalsIgnoreCase(value)) {
|
||||
return responseType;
|
||||
}
|
||||
}
|
||||
return DEFAULT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal builder class for building a CapacitorHttpUrlConnection
|
||||
*/
|
||||
public static class HttpURLConnectionBuilder {
|
||||
|
||||
public Integer connectTimeout;
|
||||
public Integer readTimeout;
|
||||
public Boolean disableRedirects;
|
||||
public JSObject headers;
|
||||
public String method;
|
||||
public URL url;
|
||||
|
||||
public CapacitorHttpUrlConnection connection;
|
||||
|
||||
public HttpURLConnectionBuilder setConnectTimeout(Integer connectTimeout) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setReadTimeout(Integer readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setDisableRedirects(Boolean disableRedirects) {
|
||||
this.disableRedirects = disableRedirects;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setHeaders(JSObject headers) {
|
||||
this.headers = headers;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setMethod(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setUrl(URL url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder openConnection() throws IOException {
|
||||
connection = new CapacitorHttpUrlConnection((HttpURLConnection) url.openConnection());
|
||||
|
||||
connection.setAllowUserInteraction(false);
|
||||
connection.setRequestMethod(method);
|
||||
|
||||
if (connectTimeout != null) connection.setConnectTimeout(connectTimeout);
|
||||
if (readTimeout != null) connection.setReadTimeout(readTimeout);
|
||||
if (disableRedirects != null) connection.setDisableRedirects(disableRedirects);
|
||||
|
||||
connection.setRequestHeaders(headers);
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setUrlParams(JSObject params) throws MalformedURLException, URISyntaxException, JSONException {
|
||||
return this.setUrlParams(params, true);
|
||||
}
|
||||
|
||||
public HttpURLConnectionBuilder setUrlParams(JSObject params, boolean shouldEncode)
|
||||
throws URISyntaxException, MalformedURLException {
|
||||
String initialQuery = url.getQuery();
|
||||
String initialQueryBuilderStr = initialQuery == null ? "" : initialQuery;
|
||||
|
||||
Iterator<String> keys = params.keys();
|
||||
|
||||
if (!keys.hasNext()) {
|
||||
return this;
|
||||
}
|
||||
|
||||
StringBuilder urlQueryBuilder = new StringBuilder(initialQueryBuilderStr);
|
||||
|
||||
// Build the new query string
|
||||
while (keys.hasNext()) {
|
||||
String key = keys.next();
|
||||
|
||||
// Attempt as JSONArray and fallback to string if it fails
|
||||
try {
|
||||
StringBuilder value = new StringBuilder();
|
||||
JSONArray arr = params.getJSONArray(key);
|
||||
for (int x = 0; x < arr.length(); x++) {
|
||||
this.addUrlParam(value, key, arr.getString(x), shouldEncode);
|
||||
if (x != arr.length() - 1) {
|
||||
value.append("&");
|
||||
}
|
||||
}
|
||||
if (urlQueryBuilder.length() > 0) {
|
||||
urlQueryBuilder.append("&");
|
||||
}
|
||||
urlQueryBuilder.append(value);
|
||||
} catch (JSONException e) {
|
||||
if (urlQueryBuilder.length() > 0) {
|
||||
urlQueryBuilder.append("&");
|
||||
}
|
||||
this.addUrlParam(urlQueryBuilder, key, params.getString(key), shouldEncode);
|
||||
}
|
||||
}
|
||||
|
||||
String urlQuery = urlQueryBuilder.toString();
|
||||
|
||||
URI uri = url.toURI();
|
||||
String unEncodedUrlString =
|
||||
uri.getScheme() +
|
||||
"://" +
|
||||
uri.getAuthority() +
|
||||
uri.getPath() +
|
||||
((!urlQuery.equals("")) ? "?" + urlQuery : "") +
|
||||
((uri.getFragment() != null) ? uri.getFragment() : "");
|
||||
this.url = new URL(unEncodedUrlString);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
private static void addUrlParam(StringBuilder sb, String key, String value, boolean shouldEncode) {
|
||||
if (shouldEncode) {
|
||||
try {
|
||||
key = URLEncoder.encode(key, "UTF-8");
|
||||
value = URLEncoder.encode(value, "UTF-8");
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException(ex.getCause());
|
||||
}
|
||||
}
|
||||
sb.append(key).append("=").append(value);
|
||||
}
|
||||
|
||||
public CapacitorHttpUrlConnection build() {
|
||||
return connection;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects.
|
||||
* Defaults to ResponseType.DEFAULT
|
||||
* @param connection The CapacitorHttpUrlConnection to respond with
|
||||
* @throws IOException Thrown if the InputStream is unable to be parsed correctly
|
||||
* @throws JSONException Thrown if the JSON is unable to be parsed
|
||||
*/
|
||||
public static JSObject buildResponse(CapacitorHttpUrlConnection connection) throws IOException, JSONException {
|
||||
return buildResponse(connection, ResponseType.DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects
|
||||
* @param connection The CapacitorHttpUrlConnection to respond with
|
||||
* @param responseType The requested ResponseType
|
||||
* @return A JSObject that contains the HTTPResponse to return to the browser
|
||||
* @throws IOException Thrown if the InputStream is unable to be parsed correctly
|
||||
* @throws JSONException Thrown if the JSON is unable to be parsed
|
||||
*/
|
||||
public static JSObject buildResponse(CapacitorHttpUrlConnection connection, ResponseType responseType)
|
||||
throws IOException, JSONException {
|
||||
int statusCode = connection.getResponseCode();
|
||||
|
||||
JSObject output = new JSObject();
|
||||
output.put("status", statusCode);
|
||||
output.put("headers", buildResponseHeaders(connection));
|
||||
output.put("url", connection.getURL());
|
||||
output.put("data", readData(connection, responseType));
|
||||
|
||||
InputStream errorStream = connection.getErrorStream();
|
||||
if (errorStream != null) {
|
||||
output.put("error", true);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the existing ICapacitorHttpUrlConnection data
|
||||
* @param connection The ICapacitorHttpUrlConnection object to read in
|
||||
* @param responseType The type of HTTP response to return to the API
|
||||
* @return The parsed data from the connection
|
||||
* @throws IOException Thrown if the InputStreams cannot be properly parsed
|
||||
* @throws JSONException Thrown if the JSON is malformed when parsing as JSON
|
||||
*/
|
||||
public static Object readData(ICapacitorHttpUrlConnection connection, ResponseType responseType) throws IOException, JSONException {
|
||||
InputStream errorStream = connection.getErrorStream();
|
||||
String contentType = connection.getHeaderField("Content-Type");
|
||||
|
||||
if (errorStream != null) {
|
||||
if (isOneOf(contentType, MimeType.APPLICATION_JSON, MimeType.APPLICATION_VND_API_JSON)) {
|
||||
return parseJSON(readStreamAsString(errorStream));
|
||||
} else {
|
||||
return readStreamAsString(errorStream);
|
||||
}
|
||||
} else if (contentType != null && contentType.contains(MimeType.APPLICATION_JSON.getValue())) {
|
||||
// backward compatibility
|
||||
return parseJSON(readStreamAsString(connection.getInputStream()));
|
||||
} else {
|
||||
InputStream stream = connection.getInputStream();
|
||||
switch (responseType) {
|
||||
case ARRAY_BUFFER:
|
||||
case BLOB:
|
||||
return readStreamAsBase64(stream);
|
||||
case JSON:
|
||||
return parseJSON(readStreamAsString(stream));
|
||||
case DOCUMENT:
|
||||
case TEXT:
|
||||
default:
|
||||
return readStreamAsString(stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function for determining if the Content-Type is a typeof an existing Mime-Type
|
||||
* @param contentType The Content-Type string to check for
|
||||
* @param mimeTypes The Mime-Type values to check against
|
||||
* @return
|
||||
*/
|
||||
public static boolean isOneOf(String contentType, MimeType... mimeTypes) {
|
||||
if (contentType != null) {
|
||||
for (MimeType mimeType : mimeTypes) {
|
||||
if (contentType.contains(mimeType.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the JSObject response headers based on the connection header map
|
||||
* @param connection The CapacitorHttpUrlConnection connection
|
||||
* @return A JSObject of the header values from the CapacitorHttpUrlConnection
|
||||
*/
|
||||
public static JSObject buildResponseHeaders(CapacitorHttpUrlConnection connection) {
|
||||
JSObject output = new JSObject();
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
|
||||
String valuesString = TextUtils.join(", ", entry.getValue());
|
||||
output.put(entry.getKey(), valuesString);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a JSObject or a JSArray based on a string-ified input
|
||||
* @param input String-ified JSON that needs parsing
|
||||
* @return A JSObject or JSArray
|
||||
* @throws JSONException thrown if the JSON is malformed
|
||||
*/
|
||||
public static Object parseJSON(String input) throws JSONException {
|
||||
JSONObject json = new JSONObject();
|
||||
try {
|
||||
if ("null".equals(input.trim())) {
|
||||
return JSONObject.NULL;
|
||||
} else if ("true".equals(input.trim())) {
|
||||
return true;
|
||||
} else if ("false".equals(input.trim())) {
|
||||
return false;
|
||||
} else if (input.trim().length() <= 0) {
|
||||
return "";
|
||||
} else if (input.trim().matches("^\".*\"$")) {
|
||||
// a string enclosed in " " is a json value, return the string without the quotes
|
||||
return input.trim().substring(1, input.trim().length() - 1);
|
||||
} else if (input.trim().matches("^-?\\d+$")) {
|
||||
return Integer.parseInt(input.trim());
|
||||
} else if (input.trim().matches("^-?\\d+(\\.\\d+)?$")) {
|
||||
return Double.parseDouble(input.trim());
|
||||
} else {
|
||||
try {
|
||||
return new JSObject(input);
|
||||
} catch (JSONException e) {
|
||||
return new JSArray(input);
|
||||
}
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string based on a base64 InputStream
|
||||
* @param in The base64 InputStream to convert to a String
|
||||
* @return String value of InputStream
|
||||
* @throws IOException thrown if the InputStream is unable to be read as base64
|
||||
*/
|
||||
public static String readStreamAsBase64(InputStream in) throws IOException {
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[1024];
|
||||
int readBytes;
|
||||
while ((readBytes = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, readBytes);
|
||||
}
|
||||
byte[] result = out.toByteArray();
|
||||
return Base64.encodeToString(result, 0, result.length, Base64.DEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string based on an InputStream
|
||||
* @param in The InputStream to convert to a String
|
||||
* @return String value of InputStream
|
||||
* @throws IOException thrown if the InputStream is unable to be read
|
||||
*/
|
||||
public static String readStreamAsString(InputStream in) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
String line = reader.readLine();
|
||||
while (line != null) {
|
||||
builder.append(line);
|
||||
line = reader.readLine();
|
||||
if (line != null) {
|
||||
builder.append(System.getProperty("line.separator"));
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an Http Request based on the PluginCall parameters
|
||||
* @param call The Capacitor PluginCall that contains the options need for an Http request
|
||||
* @param httpMethod The HTTP method that overrides the PluginCall HTTP method
|
||||
* @throws IOException throws an IO request when a connection can't be made
|
||||
* @throws URISyntaxException thrown when the URI is malformed
|
||||
* @throws JSONException thrown when the incoming JSON is malformed
|
||||
*/
|
||||
public static JSObject request(PluginCall call, String httpMethod, Bridge bridge)
|
||||
throws IOException, URISyntaxException, JSONException {
|
||||
String urlString = call.getString("url", "");
|
||||
JSObject headers = call.getObject("headers", new JSObject());
|
||||
JSObject params = call.getObject("params", new JSObject());
|
||||
Integer connectTimeout = call.getInt("connectTimeout");
|
||||
Integer readTimeout = call.getInt("readTimeout");
|
||||
Boolean disableRedirects = call.getBoolean("disableRedirects");
|
||||
Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true);
|
||||
ResponseType responseType = ResponseType.parse(call.getString("responseType"));
|
||||
String dataType = call.getString("dataType");
|
||||
|
||||
String method = httpMethod != null ? httpMethod.toUpperCase(Locale.ROOT) : call.getString("method", "GET").toUpperCase(Locale.ROOT);
|
||||
|
||||
boolean isHttpMutate = method.equals("DELETE") || method.equals("PATCH") || method.equals("POST") || method.equals("PUT");
|
||||
|
||||
URL url = new URL(urlString);
|
||||
HttpURLConnectionBuilder connectionBuilder = new HttpURLConnectionBuilder()
|
||||
.setUrl(url)
|
||||
.setMethod(method)
|
||||
.setHeaders(headers)
|
||||
.setUrlParams(params, shouldEncode)
|
||||
.setConnectTimeout(connectTimeout)
|
||||
.setReadTimeout(readTimeout)
|
||||
.setDisableRedirects(disableRedirects)
|
||||
.openConnection();
|
||||
|
||||
CapacitorHttpUrlConnection connection = connectionBuilder.build();
|
||||
|
||||
if (null != bridge && !isDomainExcludedFromSSL(bridge, url)) {
|
||||
connection.setSSLSocketFactory(bridge);
|
||||
}
|
||||
|
||||
// Set HTTP body on a non GET or HEAD request
|
||||
if (isHttpMutate) {
|
||||
JSValue data = new JSValue(call, "data");
|
||||
if (data.getValue() != null) {
|
||||
connection.setDoOutput(true);
|
||||
connection.setRequestBody(call, data, dataType);
|
||||
}
|
||||
}
|
||||
|
||||
call.getData().put("activeCapacitorHttpUrlConnection", connection);
|
||||
connection.connect();
|
||||
|
||||
JSObject response = buildResponse(connection, responseType);
|
||||
|
||||
connection.disconnect();
|
||||
call.getData().remove("activeCapacitorHttpUrlConnection");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) {
|
||||
try {
|
||||
Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning");
|
||||
Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class);
|
||||
return (Boolean) method.invoke(sslPinningImpl.getDeclaredConstructor().newInstance(), bridge, url);
|
||||
} catch (Exception ignored) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface ProgressEmitter {
|
||||
void emit(Integer bytes, Integer contentLength);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package com.getcapacitor.plugin.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* This interface was extracted from {@link CapacitorHttpUrlConnection} to enable mocking that class.
|
||||
*/
|
||||
public interface ICapacitorHttpUrlConnection {
|
||||
InputStream getErrorStream();
|
||||
|
||||
String getHeaderField(String name);
|
||||
|
||||
InputStream getInputStream() throws IOException;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package com.getcapacitor.plugin.util;
|
||||
|
||||
enum MimeType {
|
||||
APPLICATION_JSON("application/json"),
|
||||
APPLICATION_VND_API_JSON("application/vnd.api+json"), // https://jsonapi.org
|
||||
TEXT_HTML("text/html");
|
||||
|
||||
private final String value;
|
||||
|
||||
MimeType(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package com.getcapacitor.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public interface HostMask {
|
||||
boolean matches(String host);
|
||||
|
||||
class Parser {
|
||||
|
||||
private static HostMask NOTHING = new Nothing();
|
||||
|
||||
public static HostMask parse(String[] masks) {
|
||||
return masks == null ? NOTHING : HostMask.Any.parse(masks);
|
||||
}
|
||||
|
||||
public static HostMask parse(String mask) {
|
||||
return mask == null ? NOTHING : HostMask.Simple.parse(mask);
|
||||
}
|
||||
}
|
||||
|
||||
class Simple implements HostMask {
|
||||
|
||||
private final List<String> maskParts;
|
||||
|
||||
private Simple(List<String> maskParts) {
|
||||
if (maskParts == null) {
|
||||
throw new IllegalArgumentException("Mask parts can not be null");
|
||||
}
|
||||
this.maskParts = maskParts;
|
||||
}
|
||||
|
||||
static Simple parse(String mask) {
|
||||
List<String> parts = Util.splitAndReverse(mask);
|
||||
return new Simple(parts);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String host) {
|
||||
if (host == null) {
|
||||
return false;
|
||||
}
|
||||
List<String> hostParts = Util.splitAndReverse(host);
|
||||
int hostSize = hostParts.size();
|
||||
int maskSize = maskParts.size();
|
||||
if (maskSize > 1 && hostSize != maskSize) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int minSize = Math.min(hostSize, maskSize);
|
||||
|
||||
for (int i = 0; i < minSize; i++) {
|
||||
String maskPart = maskParts.get(i);
|
||||
String hostPart = hostParts.get(i);
|
||||
if (!Util.matches(maskPart, hostPart)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class Any implements HostMask {
|
||||
|
||||
private final List<? extends HostMask> masks;
|
||||
|
||||
Any(List<? extends HostMask> masks) {
|
||||
this.masks = masks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String host) {
|
||||
for (HostMask mask : masks) {
|
||||
if (mask.matches(host)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Any parse(String... rawMasks) {
|
||||
List<HostMask.Simple> masks = new ArrayList<>();
|
||||
for (String raw : rawMasks) {
|
||||
masks.add(HostMask.Simple.parse(raw));
|
||||
}
|
||||
return new Any(masks);
|
||||
}
|
||||
}
|
||||
|
||||
class Nothing implements HostMask {
|
||||
|
||||
@Override
|
||||
public boolean matches(String host) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Util {
|
||||
|
||||
static boolean matches(String mask, String string) {
|
||||
if (mask == null) {
|
||||
return false;
|
||||
} else if ("*".equals(mask)) {
|
||||
return true;
|
||||
} else if (string == null) {
|
||||
return false;
|
||||
} else {
|
||||
return mask.toUpperCase().equals(string.toUpperCase());
|
||||
}
|
||||
}
|
||||
|
||||
static List<String> splitAndReverse(String string) {
|
||||
if (string == null) {
|
||||
throw new IllegalArgumentException("Can not split null argument");
|
||||
}
|
||||
List<String> parts = Arrays.asList(string.split("\\."));
|
||||
Collections.reverse(parts);
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package com.getcapacitor.util;
|
||||
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
|
||||
public class InternalUtils {
|
||||
|
||||
public static PackageInfo getPackageInfo(PackageManager pm, String packageName) throws PackageManager.NameNotFoundException {
|
||||
return InternalUtils.getPackageInfo(pm, packageName, 0);
|
||||
}
|
||||
|
||||
public static PackageInfo getPackageInfo(PackageManager pm, String packageName, long flags)
|
||||
throws PackageManager.NameNotFoundException {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
return pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags));
|
||||
} else {
|
||||
return getPackageInfoLegacy(pm, packageName, (int) flags);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private static PackageInfo getPackageInfoLegacy(PackageManager pm, String packageName, long flags)
|
||||
throws PackageManager.NameNotFoundException {
|
||||
return pm.getPackageInfo(packageName, (int) flags);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package com.getcapacitor.util;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
/**
|
||||
* Helper methods for parsing JSON objects.
|
||||
*/
|
||||
public class JSONUtils {
|
||||
|
||||
/**
|
||||
* Get a string value from the given JSON object.
|
||||
*
|
||||
* @param jsonObject A JSON object to search
|
||||
* @param key A key to fetch from the JSON object
|
||||
* @param defaultValue A default value to return if the key cannot be found
|
||||
* @return The value at the given key in the JSON object, or the default value
|
||||
*/
|
||||
public static String getString(JSONObject jsonObject, String key, String defaultValue) {
|
||||
String k = getDeepestKey(key);
|
||||
try {
|
||||
JSONObject o = getDeepestObject(jsonObject, key);
|
||||
|
||||
String value = o.getString(k);
|
||||
if (value == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
return value;
|
||||
} catch (JSONException ignore) {
|
||||
// value was not found
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a boolean value from the given JSON object.
|
||||
*
|
||||
* @param jsonObject A JSON object to search
|
||||
* @param key A key to fetch from the JSON object
|
||||
* @param defaultValue A default value to return if the key cannot be found
|
||||
* @return The value at the given key in the JSON object, or the default value
|
||||
*/
|
||||
public static boolean getBoolean(JSONObject jsonObject, String key, boolean defaultValue) {
|
||||
String k = getDeepestKey(key);
|
||||
try {
|
||||
JSONObject o = getDeepestObject(jsonObject, key);
|
||||
|
||||
return o.getBoolean(k);
|
||||
} catch (JSONException ignore) {
|
||||
// value was not found
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an int value from the given JSON object.
|
||||
*
|
||||
* @param jsonObject A JSON object to search
|
||||
* @param key A key to fetch from the JSON object
|
||||
* @param defaultValue A default value to return if the key cannot be found
|
||||
* @return The value at the given key in the JSON object, or the default value
|
||||
*/
|
||||
public static int getInt(JSONObject jsonObject, String key, int defaultValue) {
|
||||
String k = getDeepestKey(key);
|
||||
try {
|
||||
JSONObject o = getDeepestObject(jsonObject, key);
|
||||
return o.getInt(k);
|
||||
} catch (JSONException ignore) {
|
||||
// value was not found
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a JSON object value from the given JSON object.
|
||||
*
|
||||
* @param jsonObject A JSON object to search
|
||||
* @param key A key to fetch from the JSON object
|
||||
* @return The value from the config, if exists. Null if not
|
||||
*/
|
||||
public static JSONObject getObject(JSONObject jsonObject, String key) {
|
||||
String k = getDeepestKey(key);
|
||||
try {
|
||||
JSONObject o = getDeepestObject(jsonObject, key);
|
||||
|
||||
return o.getJSONObject(k);
|
||||
} catch (JSONException ignore) {
|
||||
// value was not found
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a string array value from the given JSON object.
|
||||
*
|
||||
* @param jsonObject A JSON object to search
|
||||
* @param key A key to fetch from the JSON object
|
||||
* @param defaultValue A default value to return if the key cannot be found
|
||||
* @return The value at the given key in the JSON object, or the default value
|
||||
*/
|
||||
public static String[] getArray(JSONObject jsonObject, String key, String[] defaultValue) {
|
||||
String k = getDeepestKey(key);
|
||||
try {
|
||||
JSONObject o = getDeepestObject(jsonObject, key);
|
||||
|
||||
JSONArray a = o.getJSONArray(k);
|
||||
if (a == null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
int l = a.length();
|
||||
String[] value = new String[l];
|
||||
|
||||
for (int i = 0; i < l; i++) {
|
||||
value[i] = (String) a.get(i);
|
||||
}
|
||||
|
||||
return value;
|
||||
} catch (JSONException ignore) {
|
||||
// value was not found
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a JSON key path, gets the deepest key.
|
||||
*
|
||||
* @param key The key path
|
||||
* @return The deepest key
|
||||
*/
|
||||
private static String getDeepestKey(String key) {
|
||||
String[] parts = key.split("\\.");
|
||||
if (parts.length > 0) {
|
||||
return parts[parts.length - 1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a JSON object and key path, gets the deepest object in the path.
|
||||
*
|
||||
* @param jsonObject A JSON object
|
||||
* @param key The key path to follow
|
||||
* @return The deepest object along the key path
|
||||
* @throws JSONException Thrown if any JSON errors
|
||||
*/
|
||||
private static JSONObject getDeepestObject(JSONObject jsonObject, String key) throws JSONException {
|
||||
String[] parts = key.split("\\.");
|
||||
JSONObject o = jsonObject;
|
||||
|
||||
// Search until the second to last part of the key
|
||||
for (int i = 0; i < parts.length - 1; i++) {
|
||||
String k = parts[i];
|
||||
o = o.getJSONObject(k);
|
||||
}
|
||||
|
||||
return o;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
package com.getcapacitor.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A helper class for checking permissions.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class PermissionHelper {
|
||||
|
||||
/**
|
||||
* Checks if a list of given permissions are all granted by the user
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @param permissions Permissions to check.
|
||||
* @return True if all permissions are granted, false if at least one is not.
|
||||
*/
|
||||
public static boolean hasPermissions(Context context, String[] permissions) {
|
||||
for (String perm : permissions) {
|
||||
if (ActivityCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given permission has been defined in the AndroidManifest.xml
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @param permission A permission to check.
|
||||
* @return True if the permission has been defined in the Manifest, false if not.
|
||||
*/
|
||||
public static boolean hasDefinedPermission(Context context, String permission) {
|
||||
boolean hasPermission = false;
|
||||
String[] requestedPermissions = PermissionHelper.getManifestPermissions(context);
|
||||
if (requestedPermissions != null && requestedPermissions.length > 0) {
|
||||
List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
|
||||
ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList);
|
||||
if (requestedPermissionsArrayList.contains(permission)) {
|
||||
hasPermission = true;
|
||||
}
|
||||
}
|
||||
return hasPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether all of the given permissions have been defined in the AndroidManifest.xml
|
||||
* @param context the app context
|
||||
* @param permissions a list of permissions
|
||||
* @return true only if all permissions are defined in the AndroidManifest.xml
|
||||
*/
|
||||
public static boolean hasDefinedPermissions(Context context, String[] permissions) {
|
||||
for (String permission : permissions) {
|
||||
if (!PermissionHelper.hasDefinedPermission(context, permission)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permissions defined in AndroidManifest.xml
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @return The permissions defined in AndroidManifest.xml
|
||||
*/
|
||||
public static String[] getManifestPermissions(Context context) {
|
||||
String[] requestedPermissions = null;
|
||||
try {
|
||||
PackageManager pm = context.getPackageManager();
|
||||
PackageInfo packageInfo = InternalUtils.getPackageInfo(pm, context.getPackageName(), PackageManager.GET_PERMISSIONS);
|
||||
|
||||
if (packageInfo != null) {
|
||||
requestedPermissions = packageInfo.requestedPermissions;
|
||||
}
|
||||
} catch (Exception ex) {}
|
||||
return requestedPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml
|
||||
*
|
||||
* @since 3.0.0
|
||||
* @param neededPermissions The permissions needed.
|
||||
* @return The permissions not present in AndroidManifest.xml
|
||||
*/
|
||||
public static String[] getUndefinedPermissions(Context context, String[] neededPermissions) {
|
||||
ArrayList<String> undefinedPermissions = new ArrayList<>();
|
||||
String[] requestedPermissions = getManifestPermissions(context);
|
||||
if (requestedPermissions != null && requestedPermissions.length > 0) {
|
||||
List<String> requestedPermissionsList = Arrays.asList(requestedPermissions);
|
||||
ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList);
|
||||
for (String permission : neededPermissions) {
|
||||
if (!requestedPermissionsArrayList.contains(permission)) {
|
||||
undefinedPermissions.add(permission);
|
||||
}
|
||||
}
|
||||
String[] undefinedPermissionArray = new String[undefinedPermissions.size()];
|
||||
undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray);
|
||||
|
||||
return undefinedPermissionArray;
|
||||
}
|
||||
return neededPermissions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package com.getcapacitor.util;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
public class WebColor {
|
||||
|
||||
/**
|
||||
* Parse the color string, and return the corresponding color-int. If the string cannot be parsed, throws an IllegalArgumentException exception.
|
||||
* @param colorString The hexadecimal color string. The format is an RGB or RGBA hex string.
|
||||
* @return The corresponding color as an int.
|
||||
*/
|
||||
public static int parseColor(String colorString) {
|
||||
String formattedColor = colorString;
|
||||
if (colorString.charAt(0) != '#') {
|
||||
formattedColor = "#" + formattedColor;
|
||||
}
|
||||
|
||||
if (formattedColor.length() != 7 && formattedColor.length() != 9) {
|
||||
throw new IllegalArgumentException("The encoded color space is invalid or unknown");
|
||||
} else if (formattedColor.length() == 7) {
|
||||
return Color.parseColor(formattedColor);
|
||||
} else {
|
||||
// Convert to Android format #AARRGGBB from #RRGGBBAA
|
||||
formattedColor = "#" + formattedColor.substring(7) + formattedColor.substring(1, 7);
|
||||
return Color.parseColor(formattedColor);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context="com.getcapacitor.BridgeActivity"
|
||||
>
|
||||
|
||||
<com.getcapacitor.CapacitorWebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,13 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#F0FF1414"
|
||||
tools:context="com.getcapacitor.BridgeFragment">
|
||||
|
||||
<com.getcapacitor.CapacitorWebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent" />
|
||||
|
||||
</FrameLayout>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/no_webview_text"
|
||||
android:gravity="center"
|
||||
android:textSize="48sp" />
|
||||
</LinearLayout>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="bridge_fragment">
|
||||
<attr name="start_dir" format="string"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<color tools:ignore="UnusedResources" name="colorPrimary">#3F51B5</color>
|
||||
<color tools:ignore="UnusedResources" name="colorPrimaryDark">#303F9F</color>
|
||||
<color tools:ignore="UnusedResources" name="colorAccent">#FF4081</color>
|
||||
</resources>
|
|
@ -0,0 +1,3 @@
|
|||
<resources>
|
||||
<string name="no_webview_text">This app requires a WebView to work</string>
|
||||
</resources>
|
|
@ -0,0 +1,6 @@
|
|||
<resources>
|
||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
</style>
|
||||
</resources>
|
31
capacitor-android/package.json
Normal file
31
capacitor-android/package.json
Normal file
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"name": "@capacitor/android",
|
||||
"version": "6.1.2",
|
||||
"description": "Capacitor: Cross-platform apps with JavaScript and the web",
|
||||
"homepage": "https://capacitorjs.com",
|
||||
"author": "Ionic Team <hi@ionic.io> (https://ionic.io)",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ionic-team/capacitor.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/ionic-team/capacitor/issues"
|
||||
},
|
||||
"files": [
|
||||
"capacitor/build.gradle",
|
||||
"capacitor/lint-baseline.xml",
|
||||
"capacitor/lint.xml",
|
||||
"capacitor/proguard-rules.pro",
|
||||
"capacitor/src/main/"
|
||||
],
|
||||
"scripts": {
|
||||
"verify": "./gradlew clean lint build test -b capacitor/build.gradle"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@capacitor/core": "^6.1.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
project(':capacitor-android').projectDir = new File('./capacitor-android/capacitor')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue