Attempt to get FDroid build working

This commit is contained in:
Pieter Vander Vennet 2025-02-17 00:33:27 +01:00
parent 268f1b87e8
commit 5b4e9fa452
77 changed files with 11993 additions and 1 deletions

View file

@ -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
View 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.

View 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'
}

View 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(&quot;Accept&quot;) &amp;&amp; header.getValue().toLowerCase().contains(&quot;text/html&quot;)) {"
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(&quot;yyyyMMdd_HHmmss&quot;).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(&quot;yyyy-MM-dd&apos;T&apos;HH:mm&apos;Z&apos;&quot;);"
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=&quot;#F0FF1414&quot;"
errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
<location
file="src/main/res/layout/fragment_bridge.xml"
line="5"
column="5"/>
</issue>
</issues>

View 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>

View 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>;
}

View file

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

File diff suppressed because it is too large Load diff

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
package com.getcapacitor;
class InvalidPluginException extends Exception {
public InvalidPluginException(String s) {
super(s);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 "";
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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);
}
);
}
}

View file

@ -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 "";
}

View file

@ -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

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,8 @@
package com.getcapacitor;
/**
* An interface used in the processing of routes
*/
public interface RouteProcessor {
ProcessedRoute process(String basePath, String path);
}

View file

@ -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;
}
}

View 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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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 {
}

View file

@ -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 {};
}

View file

@ -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 "";
}

View file

@ -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 {
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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();
}
}
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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");
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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) {}
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -0,0 +1,3 @@
<resources>
<string name="no_webview_text">This app requires a WebView to work</string>
</resources>

View file

@ -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>

View 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"
}
}

View file

@ -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')