This commit is contained in:
		
							parent
							
								
									55470c090d
								
							
						
					
					
						commit
						6947a1adba
					
				
					 1260 changed files with 111297 additions and 0 deletions
				
			
		
							
								
								
									
										21
									
								
								@capacitor/android/LICENSE
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								@capacitor/android/LICENSE
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| MIT License | ||||
| 
 | ||||
| Copyright (c) 2017-present Drifty Co. | ||||
| 
 | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
| 
 | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
| 
 | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										96
									
								
								@capacitor/android/capacitor/build.gradle
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								@capacitor/android/capacitor/build.gradle
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,96 @@ | |||
| ext { | ||||
|     androidxActivityVersion = project.hasProperty('androidxActivityVersion') ? rootProject.ext.androidxActivityVersion : '1.8.0' | ||||
|     androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.6.1' | ||||
|     androidxCoordinatorLayoutVersion = project.hasProperty('androidxCoordinatorLayoutVersion') ? rootProject.ext.androidxCoordinatorLayoutVersion : '1.2.0' | ||||
|     androidxCoreVersion = project.hasProperty('androidxCoreVersion') ? rootProject.ext.androidxCoreVersion : '1.12.0' | ||||
|     androidxFragmentVersion = project.hasProperty('androidxFragmentVersion') ? rootProject.ext.androidxFragmentVersion : '1.6.2' | ||||
|     androidxWebkitVersion = project.hasProperty('androidxWebkitVersion') ? rootProject.ext.androidxWebkitVersion : '1.9.0' | ||||
|     junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' | ||||
|     androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.5' | ||||
|     androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.5.1' | ||||
|     cordovaAndroidVersion = project.hasProperty('cordovaAndroidVersion') ? rootProject.ext.cordovaAndroidVersion : '10.1.1' | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| buildscript { | ||||
|     ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '1.9.10' | ||||
|     repositories { | ||||
|         google() | ||||
|         mavenCentral() | ||||
|         maven { | ||||
|             url "https://plugins.gradle.org/m2/" | ||||
|         } | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:8.2.1' | ||||
| 
 | ||||
|         if (System.getenv("CAP_PUBLISH") == "true") { | ||||
|             classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| tasks.withType(Javadoc).all { enabled = false } | ||||
| 
 | ||||
| apply plugin: 'com.android.library' | ||||
| 
 | ||||
| if (System.getenv("CAP_PUBLISH") == "true") { | ||||
|     apply plugin: 'io.github.gradle-nexus.publish-plugin' | ||||
|     apply from: file('../scripts/publish-root.gradle') | ||||
|     apply from: file('../scripts/publish-module.gradle') | ||||
| } | ||||
| 
 | ||||
| android { | ||||
|     namespace "com.getcapacitor.android" | ||||
|     compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 34 | ||||
|     defaultConfig { | ||||
|         minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 22 | ||||
|         targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 34 | ||||
|         versionCode 1 | ||||
|         versionName "1.0" | ||||
|         consumerProguardFiles 'proguard-rules.pro' | ||||
|         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||||
|     } | ||||
|     buildTypes { | ||||
|         release { | ||||
|             minifyEnabled false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|     } | ||||
|     lintOptions { | ||||
|         baseline file("lint-baseline.xml") | ||||
|         abortOnError true | ||||
|         warningsAsErrors true | ||||
|         lintConfig file('lint.xml') | ||||
|     } | ||||
|     compileOptions { | ||||
|         sourceCompatibility JavaVersion.VERSION_17 | ||||
|         targetCompatibility JavaVersion.VERSION_17 | ||||
|     } | ||||
|     publishing { | ||||
|         singleVariant("release") | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| repositories { | ||||
|     google() | ||||
|     mavenCentral() | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     implementation fileTree(dir: 'libs', include: ['*.jar']) | ||||
|     implementation platform("org.jetbrains.kotlin:kotlin-bom:$kotlin_version") | ||||
|     implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" | ||||
|     implementation "androidx.core:core:$androidxCoreVersion" | ||||
|     implementation "androidx.activity:activity:$androidxActivityVersion" | ||||
|     implementation "androidx.fragment:fragment:$androidxFragmentVersion" | ||||
|     implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" | ||||
|     implementation "androidx.webkit:webkit:$androidxWebkitVersion" | ||||
|     testImplementation "junit:junit:$junitVersion" | ||||
|     androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" | ||||
|     androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" | ||||
|     implementation "org.apache.cordova:framework:$cordovaAndroidVersion" | ||||
|     testImplementation 'org.json:json:20231013' | ||||
|     testImplementation 'org.mockito:mockito-inline:5.2.0' | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										136
									
								
								@capacitor/android/capacitor/lint-baseline.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								@capacitor/android/capacitor/lint-baseline.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,136 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <issues format="5" by="lint 4.1.1" client="gradle" variant="all" version="4.1.1"> | ||||
| 
 | ||||
|     <issue | ||||
|         id="DefaultLocale" | ||||
|         message="Implicitly using the default locale is a common source of bugs: Use `String.format(Locale, ...)` instead" | ||||
|         errorLine1="            String msg = String.format(" | ||||
|         errorLine2="                         ^"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java" | ||||
|             line="474" | ||||
|             column="26"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="DefaultLocale" | ||||
|         message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." | ||||
|         errorLine1="                return mask.toUpperCase().equals(string.toUpperCase());" | ||||
|         errorLine2="                            ~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/util/HostMask.java" | ||||
|             line="110" | ||||
|             column="29"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="DefaultLocale" | ||||
|         message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." | ||||
|         errorLine1="                return mask.toUpperCase().equals(string.toUpperCase());" | ||||
|         errorLine2="                                                        ~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/util/HostMask.java" | ||||
|             line="110" | ||||
|             column="57"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="DefaultLocale" | ||||
|         message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." | ||||
|         errorLine1="                switch (spinnerStyle.toLowerCase()) {" | ||||
|         errorLine2="                                     ~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/Splash.java" | ||||
|             line="127" | ||||
|             column="38"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="DefaultLocale" | ||||
|         message="Implicitly using the default locale is a common source of bugs: Use `toLowerCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." | ||||
|         errorLine1="                    if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) {" | ||||
|         errorLine2="                                                                                        ~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/WebViewLocalServer.java" | ||||
|             line="327" | ||||
|             column="89"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="SimpleDateFormat" | ||||
|         message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates." | ||||
|         errorLine1="        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());" | ||||
|         errorLine2="                           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/BridgeWebChromeClient.java" | ||||
|             line="511" | ||||
|             column="28"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="SimpleDateFormat" | ||||
|         message="To get local formatting use `getDateInstance()`, `getDateTimeInstance()`, or `getTimeInstance()`, or use `new SimpleDateFormat(String template, Locale locale)` with for example `Locale.US` for ASCII dates." | ||||
|         errorLine1="        DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");" | ||||
|         errorLine2="                        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/PluginResult.java" | ||||
|             line="44" | ||||
|             column="25"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="SetJavaScriptEnabled" | ||||
|         message="Using `setJavaScriptEnabled` can introduce XSS vulnerabilities into your application, review carefully" | ||||
|         errorLine1="        settings.setJavaScriptEnabled(true);" | ||||
|         errorLine2="        ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/Bridge.java" | ||||
|             line="384" | ||||
|             column="9"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="Recycle" | ||||
|         message="This `TypedArray` should be recycled after use with `#recycle()`" | ||||
|         errorLine1="        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment);" | ||||
|         errorLine2="                               ~~~~~~~~~~~~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/BridgeFragment.java" | ||||
|             line="84" | ||||
|             column="32"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="StaticFieldLeak" | ||||
|         message="Do not place Android context classes in static fields; this is a memory leak" | ||||
|         errorLine1="    private static ImageView splashImage;" | ||||
|         errorLine2="            ~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/Splash.java" | ||||
|             line="41" | ||||
|             column="13"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="StaticFieldLeak" | ||||
|         message="Do not place Android context classes in static fields; this is a memory leak" | ||||
|         errorLine1="    private static ProgressBar spinnerBar;" | ||||
|         errorLine2="            ~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/java/com/getcapacitor/Splash.java" | ||||
|             line="42" | ||||
|             column="13"/> | ||||
|     </issue> | ||||
| 
 | ||||
|     <issue | ||||
|         id="Overdraw" | ||||
|         message="Possible overdraw: Root element paints background `#F0FF1414` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)" | ||||
|         errorLine1="    android:background="#F0FF1414"" | ||||
|         errorLine2="    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> | ||||
|         <location | ||||
|             file="src/main/res/layout/fragment_bridge.xml" | ||||
|             line="5" | ||||
|             column="5"/> | ||||
|     </issue> | ||||
| 
 | ||||
| </issues> | ||||
							
								
								
									
										9
									
								
								@capacitor/android/capacitor/lint.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								@capacitor/android/capacitor/lint.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <lint> | ||||
|     <issue id="GradleDependency" severity="ignore" /> | ||||
|     <issue id="AndroidGradlePluginVersion" severity="ignore" /> | ||||
|     <issue id="DiscouragedApi"> | ||||
|         <ignore path="src/main/java/com/getcapacitor/plugin/util/AssetUtil.java" /> | ||||
|     </issue> | ||||
|     <issue id="ObsoleteSdkInt" severity="informational" /> | ||||
| </lint> | ||||
							
								
								
									
										28
									
								
								@capacitor/android/capacitor/proguard-rules.pro
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								@capacitor/android/capacitor/proguard-rules.pro
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| # Add project specific ProGuard rules here. | ||||
| # You can control the set of applied configuration files using the | ||||
| # proguardFiles setting in build.gradle. | ||||
| # | ||||
| # For more details, see | ||||
| #   http://developer.android.com/guide/developing/tools/proguard.html | ||||
| 
 | ||||
| # Rules for Capacitor v3 plugins and annotations | ||||
|  -keep @com.getcapacitor.annotation.CapacitorPlugin public class * { | ||||
|      @com.getcapacitor.annotation.PermissionCallback <methods>; | ||||
|      @com.getcapacitor.annotation.ActivityCallback <methods>; | ||||
|      @com.getcapacitor.annotation.Permission <methods>; | ||||
|      @com.getcapacitor.PluginMethod public <methods>; | ||||
|  } | ||||
| 
 | ||||
|  -keep public class * extends com.getcapacitor.Plugin { *; } | ||||
| 
 | ||||
| # Rules for Capacitor v2 plugins and annotations | ||||
| # These are deprecated but can still be used with Capacitor for now | ||||
| -keep @com.getcapacitor.NativePlugin public class * { | ||||
|   @com.getcapacitor.PluginMethod public <methods>; | ||||
| } | ||||
| 
 | ||||
| # Rules for Cordova plugins | ||||
| -keep public class * extends org.apache.cordova.* { | ||||
|   public <methods>; | ||||
|   public <fields>; | ||||
| } | ||||
|  | @ -0,0 +1,3 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
| </manifest> | ||||
							
								
								
									
										1025
									
								
								@capacitor/android/capacitor/src/main/assets/native-bridge.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1025
									
								
								@capacitor/android/capacitor/src/main/assets/native-bridge.js
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -0,0 +1,94 @@ | |||
| // Copyright 2012 The Chromium Authors. All rights reserved. | ||||
| // Use of this source code is governed by a BSD-style license that can be | ||||
| // found in the LICENSE file. | ||||
| 
 | ||||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.res.AssetManager; | ||||
| import android.net.Uri; | ||||
| import android.util.TypedValue; | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class AndroidProtocolHandler { | ||||
| 
 | ||||
|     private Context context; | ||||
| 
 | ||||
|     public AndroidProtocolHandler(Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| 
 | ||||
|     public InputStream openAsset(String path) throws IOException { | ||||
|         return context.getAssets().open(path, AssetManager.ACCESS_STREAMING); | ||||
|     } | ||||
| 
 | ||||
|     public InputStream openResource(Uri uri) { | ||||
|         assert uri.getPath() != null; | ||||
|         // The path must be of the form ".../asset_type/asset_name.ext". | ||||
|         List<String> pathSegments = uri.getPathSegments(); | ||||
|         String assetType = pathSegments.get(pathSegments.size() - 2); | ||||
|         String assetName = pathSegments.get(pathSegments.size() - 1); | ||||
| 
 | ||||
|         // Drop the file extension. | ||||
|         assetName = assetName.split("\\.")[0]; | ||||
|         try { | ||||
|             // Use the application context for resolving the resource package name so that we do | ||||
|             // not use the browser's own resources. Note that if 'context' here belongs to the | ||||
|             // test suite, it does not have a separate application context. In that case we use | ||||
|             // the original context object directly. | ||||
|             if (context.getApplicationContext() != null) { | ||||
|                 context = context.getApplicationContext(); | ||||
|             } | ||||
|             int fieldId = getFieldId(context, assetType, assetName); | ||||
|             int valueType = getValueType(context, fieldId); | ||||
|             if (valueType == TypedValue.TYPE_STRING) { | ||||
|                 return context.getResources().openRawResource(fieldId); | ||||
|             } else { | ||||
|                 Logger.error("Asset not of type string: " + uri); | ||||
|             } | ||||
|         } catch (ClassNotFoundException | IllegalAccessException | NoSuchFieldException e) { | ||||
|             Logger.error("Unable to open resource URL: " + uri, e); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private static int getFieldId(Context context, String assetType, String assetName) | ||||
|         throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { | ||||
|         Class<?> d = context.getClassLoader().loadClass(context.getPackageName() + ".R$" + assetType); | ||||
|         java.lang.reflect.Field field = d.getField(assetName); | ||||
|         return field.getInt(null); | ||||
|     } | ||||
| 
 | ||||
|     public InputStream openFile(String filePath) throws IOException { | ||||
|         String realPath = filePath.replace(Bridge.CAPACITOR_FILE_START, ""); | ||||
|         File localFile = new File(realPath); | ||||
|         return new FileInputStream(localFile); | ||||
|     } | ||||
| 
 | ||||
|     public InputStream openContentUrl(Uri uri) throws IOException { | ||||
|         Integer port = uri.getPort(); | ||||
|         String baseUrl = uri.getScheme() + "://" + uri.getHost(); | ||||
|         if (port != -1) { | ||||
|             baseUrl += ":" + port; | ||||
|         } | ||||
|         String realPath = uri.toString().replace(baseUrl + Bridge.CAPACITOR_CONTENT_START, "content:/"); | ||||
| 
 | ||||
|         InputStream stream = null; | ||||
|         try { | ||||
|             stream = context.getContentResolver().openInputStream(Uri.parse(realPath)); | ||||
|         } catch (SecurityException e) { | ||||
|             Logger.error("Unable to open content URL: " + uri, e); | ||||
|         } | ||||
|         return stream; | ||||
|     } | ||||
| 
 | ||||
|     private static int getValueType(Context context, int fieldId) { | ||||
|         TypedValue value = new TypedValue(); | ||||
|         context.getResources().getValue(fieldId, value, true); | ||||
|         return value.type; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,61 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| 
 | ||||
| public class App { | ||||
| 
 | ||||
|     /** | ||||
|      * Interface for callbacks when app status changes. | ||||
|      */ | ||||
|     public interface AppStatusChangeListener { | ||||
|         void onAppStatusChanged(Boolean isActive); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Interface for callbacks when app is restored with pending plugin call. | ||||
|      */ | ||||
|     public interface AppRestoredListener { | ||||
|         void onAppRestored(PluginResult result); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     private AppStatusChangeListener statusChangeListener; | ||||
| 
 | ||||
|     @Nullable | ||||
|     private AppRestoredListener appRestoredListener; | ||||
| 
 | ||||
|     private boolean isActive = false; | ||||
| 
 | ||||
|     public boolean isActive() { | ||||
|         return isActive; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the object to receive callbacks. | ||||
|      * @param listener | ||||
|      */ | ||||
|     public void setStatusChangeListener(@Nullable AppStatusChangeListener listener) { | ||||
|         this.statusChangeListener = listener; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the object to receive callbacks. | ||||
|      * @param listener | ||||
|      */ | ||||
|     public void setAppRestoredListener(@Nullable AppRestoredListener listener) { | ||||
|         this.appRestoredListener = listener; | ||||
|     } | ||||
| 
 | ||||
|     protected void fireRestoredResult(PluginResult result) { | ||||
|         if (appRestoredListener != null) { | ||||
|             appRestoredListener.onAppRestored(result); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void fireStatusChange(boolean isActive) { | ||||
|         this.isActive = isActive; | ||||
|         if (statusChangeListener != null) { | ||||
|             statusChangeListener.onAppStatusChanged(isActive); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,65 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.SharedPreferences; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.security.MessageDigest; | ||||
| import java.security.NoSuchAlgorithmException; | ||||
| import java.util.Objects; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| public final class AppUUID { | ||||
| 
 | ||||
|     private static final String KEY = "CapacitorAppUUID"; | ||||
| 
 | ||||
|     public static String getAppUUID(AppCompatActivity activity) throws Exception { | ||||
|         assertAppUUID(activity); | ||||
|         return readUUID(activity); | ||||
|     } | ||||
| 
 | ||||
|     public static void regenerateAppUUID(AppCompatActivity activity) throws Exception { | ||||
|         try { | ||||
|             String uuid = generateUUID(); | ||||
|             writeUUID(activity, uuid); | ||||
|         } catch (NoSuchAlgorithmException ex) { | ||||
|             throw new Exception("Capacitor App UUID could not be generated."); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static void assertAppUUID(AppCompatActivity activity) throws Exception { | ||||
|         String uuid = readUUID(activity); | ||||
|         if (uuid.equals("")) { | ||||
|             regenerateAppUUID(activity); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private static String generateUUID() throws NoSuchAlgorithmException { | ||||
|         MessageDigest salt = MessageDigest.getInstance("SHA-256"); | ||||
|         salt.update(UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); | ||||
|         return bytesToHex(salt.digest()); | ||||
|     } | ||||
| 
 | ||||
|     private static String readUUID(AppCompatActivity activity) { | ||||
|         SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); | ||||
|         return sharedPref.getString(KEY, ""); | ||||
|     } | ||||
| 
 | ||||
|     private static void writeUUID(AppCompatActivity activity, String uuid) { | ||||
|         SharedPreferences sharedPref = activity.getPreferences(Context.MODE_PRIVATE); | ||||
|         SharedPreferences.Editor editor = sharedPref.edit(); | ||||
|         editor.putString(KEY, uuid); | ||||
|         editor.apply(); | ||||
|     } | ||||
| 
 | ||||
|     private static String bytesToHex(byte[] bytes) { | ||||
|         byte[] HEX_ARRAY = "0123456789ABCDEF".getBytes(StandardCharsets.US_ASCII); | ||||
|         byte[] hexChars = new byte[bytes.length * 2]; | ||||
|         for (int j = 0; j < bytes.length; j++) { | ||||
|             int v = bytes[j] & 0xFF; | ||||
|             hexChars[j * 2] = HEX_ARRAY[v >>> 4]; | ||||
|             hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; | ||||
|         } | ||||
|         return new String(hexChars, StandardCharsets.UTF_8); | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -0,0 +1,212 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.Intent; | ||||
| import android.content.res.Configuration; | ||||
| import android.os.Bundle; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import com.getcapacitor.android.R; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class BridgeActivity extends AppCompatActivity { | ||||
| 
 | ||||
|     protected Bridge bridge; | ||||
|     protected boolean keepRunning = true; | ||||
|     protected CapConfig config; | ||||
| 
 | ||||
|     protected int activityDepth = 0; | ||||
|     protected List<Class<? extends Plugin>> initialPlugins = new ArrayList<>(); | ||||
|     protected final Bridge.Builder bridgeBuilder = new Bridge.Builder(this); | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         bridgeBuilder.setInstanceState(savedInstanceState); | ||||
|         getApplication().setTheme(R.style.AppTheme_NoActionBar); | ||||
|         setTheme(R.style.AppTheme_NoActionBar); | ||||
|         try { | ||||
|             setContentView(R.layout.bridge_layout_main); | ||||
|         } catch (Exception ex) { | ||||
|             setContentView(R.layout.no_webview); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         PluginManager loader = new PluginManager(getAssets()); | ||||
| 
 | ||||
|         try { | ||||
|             bridgeBuilder.addPlugins(loader.loadPluginClasses()); | ||||
|         } catch (PluginLoadException ex) { | ||||
|             Logger.error("Error loading plugins.", ex); | ||||
|         } | ||||
| 
 | ||||
|         this.load(); | ||||
|     } | ||||
| 
 | ||||
|     protected void load() { | ||||
|         Logger.debug("Starting BridgeActivity"); | ||||
| 
 | ||||
|         bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create(); | ||||
| 
 | ||||
|         this.keepRunning = bridge.shouldKeepRunning(); | ||||
|         this.onNewIntent(getIntent()); | ||||
|     } | ||||
| 
 | ||||
|     public void registerPlugin(Class<? extends Plugin> plugin) { | ||||
|         bridgeBuilder.addPlugin(plugin); | ||||
|     } | ||||
| 
 | ||||
|     public void registerPlugins(List<Class<? extends Plugin>> plugins) { | ||||
|         bridgeBuilder.addPlugins(plugins); | ||||
|     } | ||||
| 
 | ||||
|     public Bridge getBridge() { | ||||
|         return this.bridge; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onSaveInstanceState(Bundle outState) { | ||||
|         super.onSaveInstanceState(outState); | ||||
|         bridge.saveInstanceState(outState); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onStart() { | ||||
|         super.onStart(); | ||||
|         activityDepth++; | ||||
|         if (this.bridge != null) { | ||||
|             this.bridge.onStart(); | ||||
|             Logger.debug("App started"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onRestart() { | ||||
|         super.onRestart(); | ||||
|         this.bridge.onRestart(); | ||||
|         Logger.debug("App restarted"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onResume() { | ||||
|         super.onResume(); | ||||
|         if (bridge != null) { | ||||
|             bridge.getApp().fireStatusChange(true); | ||||
|             this.bridge.onResume(); | ||||
|             Logger.debug("App resumed"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         super.onPause(); | ||||
|         if (bridge != null) { | ||||
|             this.bridge.onPause(); | ||||
|             Logger.debug("App paused"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onStop() { | ||||
|         super.onStop(); | ||||
|         if (bridge != null) { | ||||
|             activityDepth = Math.max(0, activityDepth - 1); | ||||
|             if (activityDepth == 0) { | ||||
|                 bridge.getApp().fireStatusChange(false); | ||||
|             } | ||||
| 
 | ||||
|             this.bridge.onStop(); | ||||
|             Logger.debug("App stopped"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (this.bridge != null) { | ||||
|             this.bridge.onDestroy(); | ||||
|             Logger.debug("App destroyed"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDetachedFromWindow() { | ||||
|         super.onDetachedFromWindow(); | ||||
|         this.bridge.onDetachedFromWindow(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles permission request results. | ||||
|      * | ||||
|      * Capacitor is backwards compatible such that plugins using legacy permission request codes | ||||
|      * may coexist with plugins using the AndroidX Activity v1.2 permission callback flow introduced | ||||
|      * in Capacitor 3.0. | ||||
|      * | ||||
|      * In this method, plugins are checked first for ownership of the legacy permission request code. | ||||
|      * If the {@link Bridge#onRequestPermissionsResult(int, String[], int[])} method indicates it has | ||||
|      * handled the permission, then the permission callback will be considered complete. Otherwise, | ||||
|      * the permission will be handled using the AndroidX Activity flow. | ||||
|      * | ||||
|      * @param requestCode the request code associated with the permission request | ||||
|      * @param permissions the Android permission strings requested | ||||
|      * @param grantResults the status result of the permission request | ||||
|      */ | ||||
|     @Override | ||||
|     public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { | ||||
|         if (this.bridge == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!bridge.onRequestPermissionsResult(requestCode, permissions, grantResults)) { | ||||
|             super.onRequestPermissionsResult(requestCode, permissions, grantResults); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handles activity results. | ||||
|      * | ||||
|      * Capacitor is backwards compatible such that plugins using legacy activity result codes | ||||
|      * may coexist with plugins using the AndroidX Activity v1.2 activity callback flow introduced | ||||
|      * in Capacitor 3.0. | ||||
|      * | ||||
|      * In this method, plugins are checked first for ownership of the legacy request code. If the | ||||
|      * {@link Bridge#onActivityResult(int, int, Intent)} method indicates it has handled the activity | ||||
|      * result, then the callback will be considered complete. Otherwise, the result will be handled | ||||
|      * using the AndroidX Activiy flow. | ||||
|      * | ||||
|      * @param requestCode the request code associated with the activity result | ||||
|      * @param resultCode the result code | ||||
|      * @param data any data included with the activity result | ||||
|      */ | ||||
|     @Override | ||||
|     protected void onActivityResult(int requestCode, int resultCode, Intent data) { | ||||
|         if (this.bridge == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (!bridge.onActivityResult(requestCode, resultCode, data)) { | ||||
|             super.onActivityResult(requestCode, resultCode, data); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onNewIntent(Intent intent) { | ||||
|         super.onNewIntent(intent); | ||||
| 
 | ||||
|         if (this.bridge == null || intent == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.bridge.onNewIntent(intent); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onConfigurationChanged(Configuration newConfig) { | ||||
|         super.onConfigurationChanged(newConfig); | ||||
| 
 | ||||
|         if (this.bridge == null) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.bridge.onConfigurationChanged(newConfig); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,134 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.res.TypedArray; | ||||
| import android.os.Bundle; | ||||
| import android.util.AttributeSet; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import androidx.fragment.app.Fragment; | ||||
| import com.getcapacitor.android.R; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * A simple {@link Fragment} subclass. | ||||
|  * Use the {@link BridgeFragment#newInstance} factory method to | ||||
|  * create an instance of this fragment. | ||||
|  */ | ||||
| public class BridgeFragment extends Fragment { | ||||
| 
 | ||||
|     private static final String ARG_START_DIR = "startDir"; | ||||
| 
 | ||||
|     protected Bridge bridge; | ||||
|     protected boolean keepRunning = true; | ||||
| 
 | ||||
|     private final List<Class<? extends Plugin>> initialPlugins = new ArrayList<>(); | ||||
|     private CapConfig config = null; | ||||
| 
 | ||||
|     private final List<WebViewListener> webViewListeners = new ArrayList<>(); | ||||
| 
 | ||||
|     public BridgeFragment() { | ||||
|         // Required empty public constructor | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Use this factory method to create a new instance of | ||||
|      * this fragment using the provided parameters. | ||||
|      * | ||||
|      * @param startDir the directory to serve content from | ||||
|      * @return A new instance of fragment BridgeFragment. | ||||
|      */ | ||||
|     public static BridgeFragment newInstance(String startDir) { | ||||
|         BridgeFragment fragment = new BridgeFragment(); | ||||
|         Bundle args = new Bundle(); | ||||
|         args.putString(ARG_START_DIR, startDir); | ||||
|         fragment.setArguments(args); | ||||
|         return fragment; | ||||
|     } | ||||
| 
 | ||||
|     public void addPlugin(Class<? extends Plugin> plugin) { | ||||
|         this.initialPlugins.add(plugin); | ||||
|     } | ||||
| 
 | ||||
|     public void setConfig(CapConfig config) { | ||||
|         this.config = config; | ||||
|     } | ||||
| 
 | ||||
|     public Bridge getBridge() { | ||||
|         return bridge; | ||||
|     } | ||||
| 
 | ||||
|     public void addWebViewListener(WebViewListener webViewListener) { | ||||
|         webViewListeners.add(webViewListener); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load the WebView and create the Bridge | ||||
|      */ | ||||
|     protected void load(Bundle savedInstanceState) { | ||||
|         Logger.debug("Loading Bridge with BridgeFragment"); | ||||
| 
 | ||||
|         Bundle args = getArguments(); | ||||
|         String startDir = null; | ||||
| 
 | ||||
|         if (args != null) { | ||||
|             startDir = getArguments().getString(ARG_START_DIR); | ||||
|         } | ||||
| 
 | ||||
|         bridge = | ||||
|             new Bridge.Builder(this) | ||||
|                 .setInstanceState(savedInstanceState) | ||||
|                 .setPlugins(initialPlugins) | ||||
|                 .setConfig(config) | ||||
|                 .addWebViewListeners(webViewListeners) | ||||
|                 .create(); | ||||
| 
 | ||||
|         if (startDir != null) { | ||||
|             bridge.setServerAssetPath(startDir); | ||||
|         } | ||||
| 
 | ||||
|         this.keepRunning = bridge.shouldKeepRunning(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) { | ||||
|         super.onInflate(context, attrs, savedInstanceState); | ||||
| 
 | ||||
|         TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment); | ||||
|         CharSequence c = a.getString(R.styleable.bridge_fragment_start_dir); | ||||
| 
 | ||||
|         if (c != null) { | ||||
|             String startDir = c.toString(); | ||||
|             Bundle args = new Bundle(); | ||||
|             args.putString(ARG_START_DIR, startDir); | ||||
|             setArguments(args); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         // Inflate the layout for this fragment | ||||
|         return inflater.inflate(R.layout.fragment_bridge, container, false); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onViewCreated(View view, Bundle savedInstanceState) { | ||||
|         super.onViewCreated(view, savedInstanceState); | ||||
|         this.load(savedInstanceState); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroy() { | ||||
|         super.onDestroy(); | ||||
|         if (this.bridge != null) { | ||||
|             this.bridge.onDestroy(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,510 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.annotation.SuppressLint; | ||||
| import android.app.Activity; | ||||
| import android.app.AlertDialog; | ||||
| import android.content.ActivityNotFoundException; | ||||
| import android.content.Intent; | ||||
| import android.net.Uri; | ||||
| import android.os.Build; | ||||
| import android.os.Environment; | ||||
| import android.provider.MediaStore; | ||||
| import android.view.View; | ||||
| import android.webkit.ConsoleMessage; | ||||
| import android.webkit.GeolocationPermissions; | ||||
| import android.webkit.JsPromptResult; | ||||
| import android.webkit.JsResult; | ||||
| import android.webkit.MimeTypeMap; | ||||
| import android.webkit.PermissionRequest; | ||||
| import android.webkit.ValueCallback; | ||||
| import android.webkit.WebChromeClient; | ||||
| import android.webkit.WebView; | ||||
| import android.widget.EditText; | ||||
| import androidx.activity.result.ActivityResult; | ||||
| import androidx.activity.result.ActivityResultCallback; | ||||
| import androidx.activity.result.ActivityResultLauncher; | ||||
| import androidx.activity.result.contract.ActivityResultContracts; | ||||
| import androidx.core.content.FileProvider; | ||||
| import com.getcapacitor.util.PermissionHelper; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.*; | ||||
| 
 | ||||
| /** | ||||
|  * Custom WebChromeClient handler, required for showing dialogs, confirms, etc. in our | ||||
|  * WebView instance. | ||||
|  */ | ||||
| public class BridgeWebChromeClient extends WebChromeClient { | ||||
| 
 | ||||
|     private interface PermissionListener { | ||||
|         void onPermissionSelect(Boolean isGranted); | ||||
|     } | ||||
| 
 | ||||
|     private interface ActivityResultListener { | ||||
|         void onActivityResult(ActivityResult result); | ||||
|     } | ||||
| 
 | ||||
|     private ActivityResultLauncher permissionLauncher; | ||||
|     private ActivityResultLauncher activityLauncher; | ||||
|     private PermissionListener permissionListener; | ||||
|     private ActivityResultListener activityListener; | ||||
| 
 | ||||
|     private Bridge bridge; | ||||
| 
 | ||||
|     public BridgeWebChromeClient(Bridge bridge) { | ||||
|         this.bridge = bridge; | ||||
| 
 | ||||
|         ActivityResultCallback<Map<String, Boolean>> permissionCallback = (Map<String, Boolean> isGranted) -> { | ||||
|             if (permissionListener != null) { | ||||
|                 boolean granted = true; | ||||
|                 for (Map.Entry<String, Boolean> permission : isGranted.entrySet()) { | ||||
|                     if (!permission.getValue()) granted = false; | ||||
|                 } | ||||
|                 permissionListener.onPermissionSelect(granted); | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         permissionLauncher = bridge.registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionCallback); | ||||
|         activityLauncher = | ||||
|             bridge.registerForActivityResult( | ||||
|                 new ActivityResultContracts.StartActivityForResult(), | ||||
|                 result -> { | ||||
|                     if (activityListener != null) { | ||||
|                         activityListener.onActivityResult(result); | ||||
|                     } | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Render web content in `view`. | ||||
|      * | ||||
|      * Both this method and {@link #onHideCustomView()} are required for | ||||
|      * rendering web content in full screen. | ||||
|      * | ||||
|      * @see <a href="https://developer.android.com/reference/android/webkit/WebChromeClient#onShowCustomView(android.view.View,%20android.webkit.WebChromeClient.CustomViewCallback)">onShowCustomView() docs</a> | ||||
|      */ | ||||
|     @Override | ||||
|     public void onShowCustomView(View view, CustomViewCallback callback) { | ||||
|         callback.onCustomViewHidden(); | ||||
|         super.onShowCustomView(view, callback); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Render web content in the original Web View again. | ||||
|      * | ||||
|      * Do not remove this method--@see #onShowCustomView(View, CustomViewCallback). | ||||
|      */ | ||||
|     @Override | ||||
|     public void onHideCustomView() { | ||||
|         super.onHideCustomView(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPermissionRequest(final PermissionRequest request) { | ||||
|         boolean isRequestPermissionRequired = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M; | ||||
| 
 | ||||
|         List<String> permissionList = new ArrayList<>(); | ||||
|         if (Arrays.asList(request.getResources()).contains("android.webkit.resource.VIDEO_CAPTURE")) { | ||||
|             permissionList.add(Manifest.permission.CAMERA); | ||||
|         } | ||||
|         if (Arrays.asList(request.getResources()).contains("android.webkit.resource.AUDIO_CAPTURE")) { | ||||
|             permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS); | ||||
|             permissionList.add(Manifest.permission.RECORD_AUDIO); | ||||
|         } | ||||
|         if (!permissionList.isEmpty() && isRequestPermissionRequired) { | ||||
|             String[] permissions = permissionList.toArray(new String[0]); | ||||
|             permissionListener = | ||||
|                 isGranted -> { | ||||
|                     if (isGranted) { | ||||
|                         request.grant(request.getResources()); | ||||
|                     } else { | ||||
|                         request.deny(); | ||||
|                     } | ||||
|                 }; | ||||
|             permissionLauncher.launch(permissions); | ||||
|         } else { | ||||
|             request.grant(request.getResources()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show the browser alert modal | ||||
|      * @param view | ||||
|      * @param url | ||||
|      * @param message | ||||
|      * @param result | ||||
|      * @return | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean onJsAlert(WebView view, String url, String message, final JsResult result) { | ||||
|         if (bridge.getActivity().isFinishing()) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()); | ||||
|         builder | ||||
|             .setMessage(message) | ||||
|             .setPositiveButton( | ||||
|                 "OK", | ||||
|                 (dialog, buttonIndex) -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.confirm(); | ||||
|                 } | ||||
|             ) | ||||
|             .setOnCancelListener( | ||||
|                 dialog -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.cancel(); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|         AlertDialog dialog = builder.create(); | ||||
| 
 | ||||
|         dialog.show(); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show the browser confirm modal | ||||
|      * @param view | ||||
|      * @param url | ||||
|      * @param message | ||||
|      * @param result | ||||
|      * @return | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean onJsConfirm(WebView view, String url, String message, final JsResult result) { | ||||
|         if (bridge.getActivity().isFinishing()) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()); | ||||
| 
 | ||||
|         builder | ||||
|             .setMessage(message) | ||||
|             .setPositiveButton( | ||||
|                 "OK", | ||||
|                 (dialog, buttonIndex) -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.confirm(); | ||||
|                 } | ||||
|             ) | ||||
|             .setNegativeButton( | ||||
|                 "Cancel", | ||||
|                 (dialog, buttonIndex) -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.cancel(); | ||||
|                 } | ||||
|             ) | ||||
|             .setOnCancelListener( | ||||
|                 dialog -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.cancel(); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|         AlertDialog dialog = builder.create(); | ||||
| 
 | ||||
|         dialog.show(); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Show the browser prompt modal | ||||
|      * @param view | ||||
|      * @param url | ||||
|      * @param message | ||||
|      * @param defaultValue | ||||
|      * @param result | ||||
|      * @return | ||||
|      */ | ||||
|     @Override | ||||
|     public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) { | ||||
|         if (bridge.getActivity().isFinishing()) { | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext()); | ||||
|         final EditText input = new EditText(view.getContext()); | ||||
| 
 | ||||
|         builder | ||||
|             .setMessage(message) | ||||
|             .setView(input) | ||||
|             .setPositiveButton( | ||||
|                 "OK", | ||||
|                 (dialog, buttonIndex) -> { | ||||
|                     dialog.dismiss(); | ||||
| 
 | ||||
|                     String inputText1 = input.getText().toString().trim(); | ||||
|                     result.confirm(inputText1); | ||||
|                 } | ||||
|             ) | ||||
|             .setNegativeButton( | ||||
|                 "Cancel", | ||||
|                 (dialog, buttonIndex) -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.cancel(); | ||||
|                 } | ||||
|             ) | ||||
|             .setOnCancelListener( | ||||
|                 dialog -> { | ||||
|                     dialog.dismiss(); | ||||
|                     result.cancel(); | ||||
|                 } | ||||
|             ); | ||||
| 
 | ||||
|         AlertDialog dialog = builder.create(); | ||||
| 
 | ||||
|         dialog.show(); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Handle the browser geolocation permission prompt | ||||
|      * @param origin | ||||
|      * @param callback | ||||
|      */ | ||||
|     @Override | ||||
|     public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) { | ||||
|         super.onGeolocationPermissionsShowPrompt(origin, callback); | ||||
|         Logger.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: " + origin); | ||||
|         final String[] geoPermissions = { Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }; | ||||
| 
 | ||||
|         if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) { | ||||
|             permissionListener = | ||||
|                 isGranted -> { | ||||
|                     if (isGranted) { | ||||
|                         callback.invoke(origin, true, false); | ||||
|                     } else { | ||||
|                         final String[] coarsePermission = { Manifest.permission.ACCESS_COARSE_LOCATION }; | ||||
|                         if ( | ||||
|                             Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && | ||||
|                             PermissionHelper.hasPermissions(bridge.getContext(), coarsePermission) | ||||
|                         ) { | ||||
|                             callback.invoke(origin, true, false); | ||||
|                         } else { | ||||
|                             callback.invoke(origin, false, false); | ||||
|                         } | ||||
|                     } | ||||
|                 }; | ||||
|             permissionLauncher.launch(geoPermissions); | ||||
|         } else { | ||||
|             // permission is already granted | ||||
|             callback.invoke(origin, true, false); | ||||
|             Logger.debug("onGeolocationPermissionsShowPrompt: has required permission"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onShowFileChooser( | ||||
|         WebView webView, | ||||
|         final ValueCallback<Uri[]> filePathCallback, | ||||
|         final FileChooserParams fileChooserParams | ||||
|     ) { | ||||
|         List<String> acceptTypes = Arrays.asList(fileChooserParams.getAcceptTypes()); | ||||
|         boolean captureEnabled = fileChooserParams.isCaptureEnabled(); | ||||
|         boolean capturePhoto = captureEnabled && acceptTypes.contains("image/*"); | ||||
|         final boolean captureVideo = captureEnabled && acceptTypes.contains("video/*"); | ||||
|         if ((capturePhoto || captureVideo)) { | ||||
|             if (isMediaCaptureSupported()) { | ||||
|                 showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo); | ||||
|             } else { | ||||
|                 permissionListener = | ||||
|                     isGranted -> { | ||||
|                         if (isGranted) { | ||||
|                             showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo); | ||||
|                         } else { | ||||
|                             Logger.warn(Logger.tags("FileChooser"), "Camera permission not granted"); | ||||
|                             filePathCallback.onReceiveValue(null); | ||||
|                         } | ||||
|                     }; | ||||
|                 final String[] camPermission = { Manifest.permission.CAMERA }; | ||||
|                 permissionLauncher.launch(camPermission); | ||||
|             } | ||||
|         } else { | ||||
|             showFilePicker(filePathCallback, fileChooserParams); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private boolean isMediaCaptureSupported() { | ||||
|         String[] permissions = { Manifest.permission.CAMERA }; | ||||
|         return ( | ||||
|             PermissionHelper.hasPermissions(bridge.getContext(), permissions) || | ||||
|             !PermissionHelper.hasDefinedPermission(bridge.getContext(), Manifest.permission.CAMERA) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private void showMediaCaptureOrFilePicker(ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams, boolean isVideo) { | ||||
|         // TODO: add support for video capture on Android M and older | ||||
|         // On Android M and lower the VIDEO_CAPTURE_INTENT (e.g.: intent.getData()) | ||||
|         // returns a file:// URI instead of the expected content:// URI. | ||||
|         // So we disable it for now because it requires a bit more work | ||||
|         boolean isVideoCaptureSupported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N; | ||||
|         boolean shown = false; | ||||
|         if (isVideo && isVideoCaptureSupported) { | ||||
|             shown = showVideoCapturePicker(filePathCallback); | ||||
|         } else { | ||||
|             shown = showImageCapturePicker(filePathCallback); | ||||
|         } | ||||
|         if (!shown) { | ||||
|             Logger.warn(Logger.tags("FileChooser"), "Media capture intent could not be launched. Falling back to default file picker."); | ||||
|             showFilePicker(filePathCallback, fileChooserParams); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("QueryPermissionsNeeded") | ||||
|     private boolean showImageCapturePicker(final ValueCallback<Uri[]> filePathCallback) { | ||||
|         Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | ||||
|         if (takePictureIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         final Uri imageFileUri; | ||||
|         try { | ||||
|             imageFileUri = createImageFileUri(); | ||||
|         } catch (Exception ex) { | ||||
|             Logger.error("Unable to create temporary media capture file: " + ex.getMessage()); | ||||
|             return false; | ||||
|         } | ||||
|         takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri); | ||||
|         activityListener = | ||||
|             activityResult -> { | ||||
|                 Uri[] result = null; | ||||
|                 if (activityResult.getResultCode() == Activity.RESULT_OK) { | ||||
|                     result = new Uri[] { imageFileUri }; | ||||
|                 } | ||||
|                 filePathCallback.onReceiveValue(result); | ||||
|             }; | ||||
|         activityLauncher.launch(takePictureIntent); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressLint("QueryPermissionsNeeded") | ||||
|     private boolean showVideoCapturePicker(final ValueCallback<Uri[]> filePathCallback) { | ||||
|         Intent takeVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); | ||||
|         if (takeVideoIntent.resolveActivity(bridge.getActivity().getPackageManager()) == null) { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         activityListener = | ||||
|             activityResult -> { | ||||
|                 Uri[] result = null; | ||||
|                 if (activityResult.getResultCode() == Activity.RESULT_OK) { | ||||
|                     result = new Uri[] { activityResult.getData().getData() }; | ||||
|                 } | ||||
|                 filePathCallback.onReceiveValue(result); | ||||
|             }; | ||||
|         activityLauncher.launch(takeVideoIntent); | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     private void showFilePicker(final ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) { | ||||
|         Intent intent = fileChooserParams.createIntent(); | ||||
|         if (fileChooserParams.getMode() == FileChooserParams.MODE_OPEN_MULTIPLE) { | ||||
|             intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); | ||||
|         } | ||||
|         if (fileChooserParams.getAcceptTypes().length > 1 || intent.getType().startsWith(".")) { | ||||
|             String[] validTypes = getValidTypes(fileChooserParams.getAcceptTypes()); | ||||
|             intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes); | ||||
|             if (intent.getType().startsWith(".")) { | ||||
|                 intent.setType(validTypes[0]); | ||||
|             } | ||||
|         } | ||||
|         try { | ||||
|             activityListener = | ||||
|                 activityResult -> { | ||||
|                     Uri[] result; | ||||
|                     Intent resultIntent = activityResult.getData(); | ||||
|                     if (activityResult.getResultCode() == Activity.RESULT_OK && resultIntent.getClipData() != null) { | ||||
|                         final int numFiles = resultIntent.getClipData().getItemCount(); | ||||
|                         result = new Uri[numFiles]; | ||||
|                         for (int i = 0; i < numFiles; i++) { | ||||
|                             result[i] = resultIntent.getClipData().getItemAt(i).getUri(); | ||||
|                         } | ||||
|                     } else { | ||||
|                         result = WebChromeClient.FileChooserParams.parseResult(activityResult.getResultCode(), resultIntent); | ||||
|                     } | ||||
|                     filePathCallback.onReceiveValue(result); | ||||
|                 }; | ||||
|             activityLauncher.launch(intent); | ||||
|         } catch (ActivityNotFoundException e) { | ||||
|             filePathCallback.onReceiveValue(null); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private String[] getValidTypes(String[] currentTypes) { | ||||
|         List<String> validTypes = new ArrayList<>(); | ||||
|         MimeTypeMap mtm = MimeTypeMap.getSingleton(); | ||||
|         for (String mime : currentTypes) { | ||||
|             if (mime.startsWith(".")) { | ||||
|                 String extension = mime.substring(1); | ||||
|                 String extensionMime = mtm.getMimeTypeFromExtension(extension); | ||||
|                 if (extensionMime != null && !validTypes.contains(extensionMime)) { | ||||
|                     validTypes.add(extensionMime); | ||||
|                 } | ||||
|             } else if (!validTypes.contains(mime)) { | ||||
|                 validTypes.add(mime); | ||||
|             } | ||||
|         } | ||||
|         Object[] validObj = validTypes.toArray(); | ||||
|         return Arrays.copyOf(validObj, validObj.length, String[].class); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onConsoleMessage(ConsoleMessage consoleMessage) { | ||||
|         String tag = Logger.tags("Console"); | ||||
|         if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) { | ||||
|             String msg = String.format( | ||||
|                 "File: %s - Line %d - Msg: %s", | ||||
|                 consoleMessage.sourceId(), | ||||
|                 consoleMessage.lineNumber(), | ||||
|                 consoleMessage.message() | ||||
|             ); | ||||
|             String level = consoleMessage.messageLevel().name(); | ||||
|             if ("ERROR".equalsIgnoreCase(level)) { | ||||
|                 Logger.error(tag, msg, null); | ||||
|             } else if ("WARNING".equalsIgnoreCase(level)) { | ||||
|                 Logger.warn(tag, msg); | ||||
|             } else if ("TIP".equalsIgnoreCase(level)) { | ||||
|                 Logger.debug(tag, msg); | ||||
|             } else { | ||||
|                 Logger.info(tag, msg); | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isValidMsg(String msg) { | ||||
|         return !( | ||||
|             msg.contains("%cresult %c") || | ||||
|             (msg.contains("%cnative %c")) || | ||||
|             msg.equalsIgnoreCase("[object Object]") || | ||||
|             msg.equalsIgnoreCase("console.groupEnd") | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private Uri createImageFileUri() throws IOException { | ||||
|         Activity activity = bridge.getActivity(); | ||||
|         File photoFile = createImageFile(activity); | ||||
|         return FileProvider.getUriForFile(activity, bridge.getContext().getPackageName() + ".fileprovider", photoFile); | ||||
|     } | ||||
| 
 | ||||
|     private File createImageFile(Activity activity) throws IOException { | ||||
|         // Create an image file name | ||||
|         String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()); | ||||
|         String imageFileName = "JPEG_" + timeStamp + "_"; | ||||
|         File storageDir = activity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); | ||||
| 
 | ||||
|         return File.createTempFile(imageFileName, ".jpg", storageDir); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,111 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.graphics.Bitmap; | ||||
| import android.net.Uri; | ||||
| import android.webkit.RenderProcessGoneDetail; | ||||
| import android.webkit.WebResourceError; | ||||
| import android.webkit.WebResourceRequest; | ||||
| import android.webkit.WebResourceResponse; | ||||
| import android.webkit.WebView; | ||||
| import android.webkit.WebViewClient; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public class BridgeWebViewClient extends WebViewClient { | ||||
| 
 | ||||
|     private Bridge bridge; | ||||
| 
 | ||||
|     public BridgeWebViewClient(Bridge bridge) { | ||||
|         this.bridge = bridge; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { | ||||
|         return bridge.getLocalServer().shouldInterceptRequest(request); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { | ||||
|         Uri url = request.getUrl(); | ||||
|         return bridge.launchIntent(url); | ||||
|     } | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public boolean shouldOverrideUrlLoading(WebView view, String url) { | ||||
|         return bridge.launchIntent(Uri.parse(url)); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPageFinished(WebView view, String url) { | ||||
|         super.onPageFinished(view, url); | ||||
|         List<WebViewListener> webViewListeners = bridge.getWebViewListeners(); | ||||
| 
 | ||||
|         if (webViewListeners != null && view.getProgress() == 100) { | ||||
|             for (WebViewListener listener : bridge.getWebViewListeners()) { | ||||
|                 listener.onPageLoaded(view); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { | ||||
|         super.onReceivedError(view, request, error); | ||||
| 
 | ||||
|         List<WebViewListener> webViewListeners = bridge.getWebViewListeners(); | ||||
|         if (webViewListeners != null) { | ||||
|             for (WebViewListener listener : bridge.getWebViewListeners()) { | ||||
|                 listener.onReceivedError(view); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         String errorPath = bridge.getErrorUrl(); | ||||
|         if (errorPath != null && request.isForMainFrame()) { | ||||
|             view.loadUrl(errorPath); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onPageStarted(WebView view, String url, Bitmap favicon) { | ||||
|         super.onPageStarted(view, url, favicon); | ||||
|         bridge.reset(); | ||||
|         List<WebViewListener> webViewListeners = bridge.getWebViewListeners(); | ||||
| 
 | ||||
|         if (webViewListeners != null) { | ||||
|             for (WebViewListener listener : bridge.getWebViewListeners()) { | ||||
|                 listener.onPageStarted(view); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onReceivedHttpError(WebView view, WebResourceRequest request, WebResourceResponse errorResponse) { | ||||
|         super.onReceivedHttpError(view, request, errorResponse); | ||||
| 
 | ||||
|         List<WebViewListener> webViewListeners = bridge.getWebViewListeners(); | ||||
|         if (webViewListeners != null) { | ||||
|             for (WebViewListener listener : bridge.getWebViewListeners()) { | ||||
|                 listener.onReceivedHttpError(view); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         String errorPath = bridge.getErrorUrl(); | ||||
|         if (errorPath != null && request.isForMainFrame()) { | ||||
|             view.loadUrl(errorPath); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean onRenderProcessGone(WebView view, RenderProcessGoneDetail detail) { | ||||
|         super.onRenderProcessGone(view, detail); | ||||
|         boolean result = false; | ||||
| 
 | ||||
|         List<WebViewListener> webViewListeners = bridge.getWebViewListeners(); | ||||
|         if (webViewListeners != null) { | ||||
|             for (WebViewListener listener : bridge.getWebViewListeners()) { | ||||
|                 result = listener.onRenderProcessGone(view, detail) || result; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return result; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,683 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import static com.getcapacitor.Bridge.CAPACITOR_HTTPS_SCHEME; | ||||
| import static com.getcapacitor.Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION; | ||||
| import static com.getcapacitor.Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION; | ||||
| import static com.getcapacitor.Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION; | ||||
| import static com.getcapacitor.Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION; | ||||
| import static com.getcapacitor.FileUtils.readFileFromAssets; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.pm.ApplicationInfo; | ||||
| import android.content.res.AssetManager; | ||||
| import androidx.annotation.Nullable; | ||||
| import com.getcapacitor.util.JSONUtils; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.util.Arrays; | ||||
| import java.util.HashMap; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| /** | ||||
|  * Represents the configuration options for Capacitor | ||||
|  */ | ||||
| public class CapConfig { | ||||
| 
 | ||||
|     private static final String LOG_BEHAVIOR_NONE = "none"; | ||||
|     private static final String LOG_BEHAVIOR_DEBUG = "debug"; | ||||
|     private static final String LOG_BEHAVIOR_PRODUCTION = "production"; | ||||
| 
 | ||||
|     // Server Config | ||||
|     private boolean html5mode = true; | ||||
|     private String serverUrl; | ||||
|     private String hostname = "localhost"; | ||||
|     private String androidScheme = CAPACITOR_HTTPS_SCHEME; | ||||
|     private String[] allowNavigation; | ||||
| 
 | ||||
|     // Android Config | ||||
|     private String overriddenUserAgentString; | ||||
|     private String appendedUserAgentString; | ||||
|     private String backgroundColor; | ||||
|     private boolean allowMixedContent = false; | ||||
|     private boolean captureInput = false; | ||||
|     private boolean webContentsDebuggingEnabled = false; | ||||
|     private boolean loggingEnabled = true; | ||||
|     private boolean initialFocus = true; | ||||
|     private boolean useLegacyBridge = false; | ||||
|     private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION; | ||||
|     private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION; | ||||
|     private String errorPath; | ||||
|     private boolean zoomableWebView = false; | ||||
| 
 | ||||
|     // Embedded | ||||
|     private String startPath; | ||||
| 
 | ||||
|     // Plugins | ||||
|     private Map<String, PluginConfig> pluginsConfiguration = null; | ||||
| 
 | ||||
|     // Config Object JSON (legacy) | ||||
|     private JSONObject configJSON = new JSONObject(); | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs an empty config file. | ||||
|      */ | ||||
|     private CapConfig() {} | ||||
| 
 | ||||
|     /** | ||||
|      * Get an instance of the Config file object. | ||||
|      * @deprecated use {@link #loadDefault(Context)} to load an instance of the Config object | ||||
|      * from the capacitor.config.json file, or use the {@link CapConfig.Builder} to construct | ||||
|      * a CapConfig for embedded use. | ||||
|      * | ||||
|      * @param assetManager The AssetManager used to load the config file | ||||
|      * @param config JSON describing a configuration to use | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public CapConfig(AssetManager assetManager, JSONObject config) { | ||||
|         if (config != null) { | ||||
|             this.configJSON = config; | ||||
|         } else { | ||||
|             // Load the capacitor.config.json | ||||
|             loadConfigFromAssets(assetManager, null); | ||||
|         } | ||||
| 
 | ||||
|         deserializeConfig(null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a Capacitor Configuration from config.json file. | ||||
|      * | ||||
|      * @param context The context. | ||||
|      * @return A loaded config file, if successful. | ||||
|      */ | ||||
|     public static CapConfig loadDefault(Context context) { | ||||
|         CapConfig config = new CapConfig(); | ||||
| 
 | ||||
|         if (context == null) { | ||||
|             Logger.error("Capacitor Config could not be created from file. Context must not be null."); | ||||
|             return config; | ||||
|         } | ||||
| 
 | ||||
|         config.loadConfigFromAssets(context.getAssets(), null); | ||||
|         config.deserializeConfig(context); | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a Capacitor Configuration from config.json file within the app assets. | ||||
|      * | ||||
|      * @param context The context. | ||||
|      * @param path A path relative to the root assets directory. | ||||
|      * @return A loaded config file, if successful. | ||||
|      */ | ||||
|     public static CapConfig loadFromAssets(Context context, String path) { | ||||
|         CapConfig config = new CapConfig(); | ||||
| 
 | ||||
|         if (context == null) { | ||||
|             Logger.error("Capacitor Config could not be created from file. Context must not be null."); | ||||
|             return config; | ||||
|         } | ||||
| 
 | ||||
|         config.loadConfigFromAssets(context.getAssets(), path); | ||||
|         config.deserializeConfig(context); | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a Capacitor Configuration from config.json file within the app file-space. | ||||
|      * | ||||
|      * @param context The context. | ||||
|      * @param path A path relative to the root of the app file-space. | ||||
|      * @return A loaded config file, if successful. | ||||
|      */ | ||||
|     public static CapConfig loadFromFile(Context context, String path) { | ||||
|         CapConfig config = new CapConfig(); | ||||
| 
 | ||||
|         if (context == null) { | ||||
|             Logger.error("Capacitor Config could not be created from file. Context must not be null."); | ||||
|             return config; | ||||
|         } | ||||
| 
 | ||||
|         config.loadConfigFromFile(path); | ||||
|         config.deserializeConfig(context); | ||||
|         return config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a Capacitor Configuration using ConfigBuilder. | ||||
|      * | ||||
|      * @param builder A config builder initialized with values | ||||
|      */ | ||||
|     private CapConfig(Builder builder) { | ||||
|         // Server Config | ||||
|         this.html5mode = builder.html5mode; | ||||
|         this.serverUrl = builder.serverUrl; | ||||
|         this.hostname = builder.hostname; | ||||
| 
 | ||||
|         if (this.validateScheme(builder.androidScheme)) { | ||||
|             this.androidScheme = builder.androidScheme; | ||||
|         } | ||||
| 
 | ||||
|         this.allowNavigation = builder.allowNavigation; | ||||
| 
 | ||||
|         // Android Config | ||||
|         this.overriddenUserAgentString = builder.overriddenUserAgentString; | ||||
|         this.appendedUserAgentString = builder.appendedUserAgentString; | ||||
|         this.backgroundColor = builder.backgroundColor; | ||||
|         this.allowMixedContent = builder.allowMixedContent; | ||||
|         this.captureInput = builder.captureInput; | ||||
|         this.webContentsDebuggingEnabled = builder.webContentsDebuggingEnabled; | ||||
|         this.loggingEnabled = builder.loggingEnabled; | ||||
|         this.initialFocus = builder.initialFocus; | ||||
|         this.useLegacyBridge = builder.useLegacyBridge; | ||||
|         this.minWebViewVersion = builder.minWebViewVersion; | ||||
|         this.minHuaweiWebViewVersion = builder.minHuaweiWebViewVersion; | ||||
|         this.errorPath = builder.errorPath; | ||||
|         this.zoomableWebView = builder.zoomableWebView; | ||||
| 
 | ||||
|         // Embedded | ||||
|         this.startPath = builder.startPath; | ||||
| 
 | ||||
|         // Plugins Config | ||||
|         this.pluginsConfiguration = builder.pluginsConfiguration; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a Capacitor Configuration JSON file into a Capacitor Configuration object. | ||||
|      * An optional path string can be provided to look for the config in a subdirectory path. | ||||
|      */ | ||||
|     private void loadConfigFromAssets(AssetManager assetManager, String path) { | ||||
|         if (path == null) { | ||||
|             path = ""; | ||||
|         } else { | ||||
|             // Add slash at the end to form a proper file path if going deeper in assets dir | ||||
|             if (path.charAt(path.length() - 1) != '/') { | ||||
|                 path = path + "/"; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             String jsonString = readFileFromAssets(assetManager, path + "capacitor.config.json"); | ||||
|             configJSON = new JSONObject(jsonString); | ||||
|         } catch (IOException ex) { | ||||
|             Logger.error("Unable to load capacitor.config.json. Run npx cap copy first", ex); | ||||
|         } catch (JSONException ex) { | ||||
|             Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Loads a Capacitor Configuration JSON file into a Capacitor Configuration object. | ||||
|      * An optional path string can be provided to look for the config in a subdirectory path. | ||||
|      */ | ||||
|     private void loadConfigFromFile(String path) { | ||||
|         if (path == null) { | ||||
|             path = ""; | ||||
|         } else { | ||||
|             // Add slash at the end to form a proper file path if going deeper in assets dir | ||||
|             if (path.charAt(path.length() - 1) != '/') { | ||||
|                 path = path + "/"; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             File configFile = new File(path + "capacitor.config.json"); | ||||
|             String jsonString = FileUtils.readFileFromDisk(configFile); | ||||
|             configJSON = new JSONObject(jsonString); | ||||
|         } catch (JSONException ex) { | ||||
|             Logger.error("Unable to parse capacitor.config.json. Make sure it's valid json", ex); | ||||
|         } catch (IOException ex) { | ||||
|             Logger.error("Unable to load capacitor.config.json.", ex); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Deserializes the config from JSON into a Capacitor Configuration object. | ||||
|      */ | ||||
|     private void deserializeConfig(@Nullable Context context) { | ||||
|         boolean isDebug = context != null && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; | ||||
| 
 | ||||
|         // Server | ||||
|         html5mode = JSONUtils.getBoolean(configJSON, "server.html5mode", html5mode); | ||||
|         serverUrl = JSONUtils.getString(configJSON, "server.url", null); | ||||
|         hostname = JSONUtils.getString(configJSON, "server.hostname", hostname); | ||||
|         errorPath = JSONUtils.getString(configJSON, "server.errorPath", null); | ||||
| 
 | ||||
|         String configSchema = JSONUtils.getString(configJSON, "server.androidScheme", androidScheme); | ||||
|         if (this.validateScheme(configSchema)) { | ||||
|             androidScheme = configSchema; | ||||
|         } | ||||
| 
 | ||||
|         allowNavigation = JSONUtils.getArray(configJSON, "server.allowNavigation", null); | ||||
| 
 | ||||
|         // Android | ||||
|         overriddenUserAgentString = | ||||
|             JSONUtils.getString(configJSON, "android.overrideUserAgent", JSONUtils.getString(configJSON, "overrideUserAgent", null)); | ||||
|         appendedUserAgentString = | ||||
|             JSONUtils.getString(configJSON, "android.appendUserAgent", JSONUtils.getString(configJSON, "appendUserAgent", null)); | ||||
|         backgroundColor = | ||||
|             JSONUtils.getString(configJSON, "android.backgroundColor", JSONUtils.getString(configJSON, "backgroundColor", null)); | ||||
|         allowMixedContent = | ||||
|             JSONUtils.getBoolean( | ||||
|                 configJSON, | ||||
|                 "android.allowMixedContent", | ||||
|                 JSONUtils.getBoolean(configJSON, "allowMixedContent", allowMixedContent) | ||||
|             ); | ||||
|         minWebViewVersion = JSONUtils.getInt(configJSON, "android.minWebViewVersion", DEFAULT_ANDROID_WEBVIEW_VERSION); | ||||
|         minHuaweiWebViewVersion = JSONUtils.getInt(configJSON, "android.minHuaweiWebViewVersion", DEFAULT_HUAWEI_WEBVIEW_VERSION); | ||||
|         captureInput = JSONUtils.getBoolean(configJSON, "android.captureInput", captureInput); | ||||
|         useLegacyBridge = JSONUtils.getBoolean(configJSON, "android.useLegacyBridge", useLegacyBridge); | ||||
|         webContentsDebuggingEnabled = JSONUtils.getBoolean(configJSON, "android.webContentsDebuggingEnabled", isDebug); | ||||
|         zoomableWebView = JSONUtils.getBoolean(configJSON, "android.zoomEnabled", JSONUtils.getBoolean(configJSON, "zoomEnabled", false)); | ||||
| 
 | ||||
|         String logBehavior = JSONUtils.getString( | ||||
|             configJSON, | ||||
|             "android.loggingBehavior", | ||||
|             JSONUtils.getString(configJSON, "loggingBehavior", LOG_BEHAVIOR_DEBUG) | ||||
|         ); | ||||
|         switch (logBehavior.toLowerCase(Locale.ROOT)) { | ||||
|             case LOG_BEHAVIOR_PRODUCTION: | ||||
|                 loggingEnabled = true; | ||||
|                 break; | ||||
|             case LOG_BEHAVIOR_NONE: | ||||
|                 loggingEnabled = false; | ||||
|                 break; | ||||
|             default: // LOG_BEHAVIOR_DEBUG | ||||
|                 loggingEnabled = isDebug; | ||||
|         } | ||||
| 
 | ||||
|         initialFocus = JSONUtils.getBoolean(configJSON, "android.initialFocus", initialFocus); | ||||
| 
 | ||||
|         // Plugins | ||||
|         pluginsConfiguration = deserializePluginsConfig(JSONUtils.getObject(configJSON, "plugins")); | ||||
|     } | ||||
| 
 | ||||
|     private boolean validateScheme(String scheme) { | ||||
|         List<String> invalidSchemes = Arrays.asList("file", "ftp", "ftps", "ws", "wss", "about", "blob", "data"); | ||||
|         if (invalidSchemes.contains(scheme)) { | ||||
|             Logger.warn(scheme + " is not an allowed scheme.  Defaulting to https."); | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         // Non-http(s) schemes are not allowed to modify the URL path as of Android Webview 117 | ||||
|         if (!scheme.equals("http") && !scheme.equals("https")) { | ||||
|             Logger.warn( | ||||
|                 "Using a non-standard scheme: " + scheme + " for Android. This is known to cause issues as of Android Webview 117." | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isHTML5Mode() { | ||||
|         return html5mode; | ||||
|     } | ||||
| 
 | ||||
|     public String getServerUrl() { | ||||
|         return serverUrl; | ||||
|     } | ||||
| 
 | ||||
|     public String getErrorPath() { | ||||
|         return errorPath; | ||||
|     } | ||||
| 
 | ||||
|     public String getHostname() { | ||||
|         return hostname; | ||||
|     } | ||||
| 
 | ||||
|     public String getStartPath() { | ||||
|         return startPath; | ||||
|     } | ||||
| 
 | ||||
|     public String getAndroidScheme() { | ||||
|         return androidScheme; | ||||
|     } | ||||
| 
 | ||||
|     public String[] getAllowNavigation() { | ||||
|         return allowNavigation; | ||||
|     } | ||||
| 
 | ||||
|     public String getOverriddenUserAgentString() { | ||||
|         return overriddenUserAgentString; | ||||
|     } | ||||
| 
 | ||||
|     public String getAppendedUserAgentString() { | ||||
|         return appendedUserAgentString; | ||||
|     } | ||||
| 
 | ||||
|     public String getBackgroundColor() { | ||||
|         return backgroundColor; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isMixedContentAllowed() { | ||||
|         return allowMixedContent; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isInputCaptured() { | ||||
|         return captureInput; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isWebContentsDebuggingEnabled() { | ||||
|         return webContentsDebuggingEnabled; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isZoomableWebView() { | ||||
|         return zoomableWebView; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isLoggingEnabled() { | ||||
|         return loggingEnabled; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isInitialFocus() { | ||||
|         return initialFocus; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isUsingLegacyBridge() { | ||||
|         return useLegacyBridge; | ||||
|     } | ||||
| 
 | ||||
|     public int getMinWebViewVersion() { | ||||
|         if (minWebViewVersion < MINIMUM_ANDROID_WEBVIEW_VERSION) { | ||||
|             Logger.warn("Specified minimum webview version is too low, defaulting to " + MINIMUM_ANDROID_WEBVIEW_VERSION); | ||||
|             return MINIMUM_ANDROID_WEBVIEW_VERSION; | ||||
|         } | ||||
| 
 | ||||
|         return minWebViewVersion; | ||||
|     } | ||||
| 
 | ||||
|     public int getMinHuaweiWebViewVersion() { | ||||
|         if (minHuaweiWebViewVersion < MINIMUM_HUAWEI_WEBVIEW_VERSION) { | ||||
|             Logger.warn("Specified minimum Huawei webview version is too low, defaulting to " + MINIMUM_HUAWEI_WEBVIEW_VERSION); | ||||
|             return MINIMUM_HUAWEI_WEBVIEW_VERSION; | ||||
|         } | ||||
| 
 | ||||
|         return minHuaweiWebViewVersion; | ||||
|     } | ||||
| 
 | ||||
|     public PluginConfig getPluginConfiguration(String pluginId) { | ||||
|         PluginConfig pluginConfig = pluginsConfiguration.get(pluginId); | ||||
|         if (pluginConfig == null) { | ||||
|             pluginConfig = new PluginConfig(new JSONObject()); | ||||
|         } | ||||
| 
 | ||||
|         return pluginConfig; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a JSON object value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getObject(String)}  to access plugin config values. | ||||
|      * For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public JSONObject getObject(String key) { | ||||
|         try { | ||||
|             return configJSON.getJSONObject(key); | ||||
|         } catch (Exception ex) {} | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config | ||||
|      * values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public String getString(String key) { | ||||
|         return JSONUtils.getString(configJSON, key, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getString(String, String)} to access plugin config | ||||
|      * values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public String getString(String key, String defaultValue) { | ||||
|         return JSONUtils.getString(configJSON, key, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a boolean value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getBoolean(String, boolean)} to access plugin config | ||||
|      * values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public boolean getBoolean(String key, boolean defaultValue) { | ||||
|         return JSONUtils.getBoolean(configJSON, key, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an integer value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getInt(String, int)}  to access the plugin config | ||||
|      * values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public int getInt(String key, int defaultValue) { | ||||
|         return JSONUtils.getInt(configJSON, key, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string array value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getArray(String)}  to access the plugin config | ||||
|      * values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public String[] getArray(String key) { | ||||
|         return JSONUtils.getArray(configJSON, key, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string array value from the Capacitor config. | ||||
|      * @deprecated use {@link PluginConfig#getArray(String, String[])}  to access the plugin | ||||
|      * config values. For main Capacitor config values, use the appropriate getter. | ||||
|      * | ||||
|      * @param key A key to fetch from the config | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public String[] getArray(String key, String[] defaultValue) { | ||||
|         return JSONUtils.getArray(configJSON, key, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     private static Map<String, PluginConfig> deserializePluginsConfig(JSONObject pluginsConfig) { | ||||
|         Map<String, PluginConfig> pluginsMap = new HashMap<>(); | ||||
| 
 | ||||
|         // return an empty map if there is no pluginsConfig json | ||||
|         if (pluginsConfig == null) { | ||||
|             return pluginsMap; | ||||
|         } | ||||
| 
 | ||||
|         Iterator<String> pluginIds = pluginsConfig.keys(); | ||||
| 
 | ||||
|         while (pluginIds.hasNext()) { | ||||
|             String pluginId = pluginIds.next(); | ||||
|             JSONObject value = null; | ||||
| 
 | ||||
|             try { | ||||
|                 value = pluginsConfig.getJSONObject(pluginId); | ||||
|                 PluginConfig pluginConfig = new PluginConfig(value); | ||||
|                 pluginsMap.put(pluginId, pluginConfig); | ||||
|             } catch (JSONException e) { | ||||
|                 e.printStackTrace(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return pluginsMap; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds a Capacitor Configuration in code | ||||
|      */ | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private Context context; | ||||
| 
 | ||||
|         // Server Config Values | ||||
|         private boolean html5mode = true; | ||||
|         private String serverUrl; | ||||
|         private String errorPath; | ||||
|         private String hostname = "localhost"; | ||||
|         private String androidScheme = CAPACITOR_HTTPS_SCHEME; | ||||
|         private String[] allowNavigation; | ||||
| 
 | ||||
|         // Android Config Values | ||||
|         private String overriddenUserAgentString; | ||||
|         private String appendedUserAgentString; | ||||
|         private String backgroundColor; | ||||
|         private boolean allowMixedContent = false; | ||||
|         private boolean captureInput = false; | ||||
|         private Boolean webContentsDebuggingEnabled = null; | ||||
|         private boolean loggingEnabled = true; | ||||
|         private boolean initialFocus = false; | ||||
|         private boolean useLegacyBridge = false; | ||||
|         private int minWebViewVersion = DEFAULT_ANDROID_WEBVIEW_VERSION; | ||||
|         private int minHuaweiWebViewVersion = DEFAULT_HUAWEI_WEBVIEW_VERSION; | ||||
|         private boolean zoomableWebView = false; | ||||
| 
 | ||||
|         // Embedded | ||||
|         private String startPath = null; | ||||
| 
 | ||||
|         // Plugins Config Object | ||||
|         private Map<String, PluginConfig> pluginsConfiguration = new HashMap<>(); | ||||
| 
 | ||||
|         /** | ||||
|          * Constructs a new CapConfig Builder. | ||||
|          * | ||||
|          * @param context The context | ||||
|          */ | ||||
|         public Builder(Context context) { | ||||
|             this.context = context; | ||||
|         } | ||||
| 
 | ||||
|         /** | ||||
|          * Builds a Capacitor Config from the builder. | ||||
|          * | ||||
|          * @return A new Capacitor Config | ||||
|          */ | ||||
|         public CapConfig create() { | ||||
|             if (webContentsDebuggingEnabled == null) { | ||||
|                 webContentsDebuggingEnabled = (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; | ||||
|             } | ||||
| 
 | ||||
|             return new CapConfig(this); | ||||
|         } | ||||
| 
 | ||||
|         public Builder setPluginsConfiguration(JSONObject pluginsConfiguration) { | ||||
|             this.pluginsConfiguration = deserializePluginsConfig(pluginsConfiguration); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setHTML5mode(boolean html5mode) { | ||||
|             this.html5mode = html5mode; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setServerUrl(String serverUrl) { | ||||
|             this.serverUrl = serverUrl; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setErrorPath(String errorPath) { | ||||
|             this.errorPath = errorPath; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setHostname(String hostname) { | ||||
|             this.hostname = hostname; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setStartPath(String path) { | ||||
|             this.startPath = path; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setAndroidScheme(String androidScheme) { | ||||
|             this.androidScheme = androidScheme; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setAllowNavigation(String[] allowNavigation) { | ||||
|             this.allowNavigation = allowNavigation; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setOverriddenUserAgentString(String overriddenUserAgentString) { | ||||
|             this.overriddenUserAgentString = overriddenUserAgentString; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setAppendedUserAgentString(String appendedUserAgentString) { | ||||
|             this.appendedUserAgentString = appendedUserAgentString; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setBackgroundColor(String backgroundColor) { | ||||
|             this.backgroundColor = backgroundColor; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setAllowMixedContent(boolean allowMixedContent) { | ||||
|             this.allowMixedContent = allowMixedContent; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setCaptureInput(boolean captureInput) { | ||||
|             this.captureInput = captureInput; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setUseLegacyBridge(boolean useLegacyBridge) { | ||||
|             this.useLegacyBridge = useLegacyBridge; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setWebContentsDebuggingEnabled(boolean webContentsDebuggingEnabled) { | ||||
|             this.webContentsDebuggingEnabled = webContentsDebuggingEnabled; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setZoomableWebView(boolean zoomableWebView) { | ||||
|             this.zoomableWebView = zoomableWebView; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setLoggingEnabled(boolean enabled) { | ||||
|             this.loggingEnabled = enabled; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setInitialFocus(boolean focus) { | ||||
|             this.initialFocus = focus; | ||||
|             return this; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,52 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.util.AttributeSet; | ||||
| import android.view.KeyEvent; | ||||
| import android.view.inputmethod.BaseInputConnection; | ||||
| import android.view.inputmethod.EditorInfo; | ||||
| import android.view.inputmethod.InputConnection; | ||||
| import android.webkit.WebView; | ||||
| 
 | ||||
| public class CapacitorWebView extends WebView { | ||||
| 
 | ||||
|     private BaseInputConnection capInputConnection; | ||||
|     private Bridge bridge; | ||||
| 
 | ||||
|     public CapacitorWebView(Context context, AttributeSet attrs) { | ||||
|         super(context, attrs); | ||||
|     } | ||||
| 
 | ||||
|     public void setBridge(Bridge bridge) { | ||||
|         this.bridge = bridge; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public InputConnection onCreateInputConnection(EditorInfo outAttrs) { | ||||
|         CapConfig config; | ||||
|         if (bridge != null) { | ||||
|             config = bridge.getConfig(); | ||||
|         } else { | ||||
|             config = CapConfig.loadDefault(getContext()); | ||||
|         } | ||||
| 
 | ||||
|         boolean captureInput = config.isInputCaptured(); | ||||
|         if (captureInput) { | ||||
|             if (capInputConnection == null) { | ||||
|                 capInputConnection = new BaseInputConnection(this, false); | ||||
|             } | ||||
|             return capInputConnection; | ||||
|         } | ||||
|         return super.onCreateInputConnection(outAttrs); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @SuppressWarnings("deprecation") | ||||
|     public boolean dispatchKeyEvent(KeyEvent event) { | ||||
|         if (event.getAction() == KeyEvent.ACTION_MULTIPLE) { | ||||
|             evaluateJavascript("document.activeElement.value = document.activeElement.value + '" + event.getCharacters() + "';", null); | ||||
|             return false; | ||||
|         } | ||||
|         return super.dispatchKeyEvent(event); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,292 @@ | |||
| /** | ||||
|  * Portions adopted from react-native-image-crop-picker | ||||
|  * | ||||
|  * MIT License | ||||
| 
 | ||||
|  * Copyright (c) 2017 Ivan Pusic | ||||
| 
 | ||||
|  * Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
|  * of this software and associated documentation files (the "Software"), to deal | ||||
|  * in the Software without restriction, including without limitation the rights | ||||
|  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
|  * copies of the Software, and to permit persons to whom the Software is | ||||
|  * furnished to do so, subject to the following conditions: | ||||
|  * | ||||
|  * The above copyright notice and this permission notice shall be included in all | ||||
|  * copies or substantial portions of the Software. | ||||
|  * | ||||
|  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
|  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
|  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
|  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
|  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
|  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
|  * SOFTWARE. | ||||
|  */ | ||||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.ContentUris; | ||||
| import android.content.Context; | ||||
| import android.content.res.AssetManager; | ||||
| import android.database.Cursor; | ||||
| import android.net.Uri; | ||||
| import android.os.Environment; | ||||
| import android.provider.DocumentsContract; | ||||
| import android.provider.MediaStore; | ||||
| import android.provider.OpenableColumns; | ||||
| import java.io.BufferedReader; | ||||
| import java.io.File; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.FileReader; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.InputStreamReader; | ||||
| 
 | ||||
| /** | ||||
|  * Common File utilities, such as resolve content URIs and | ||||
|  * creating portable web paths from low-level files | ||||
|  */ | ||||
| public class FileUtils { | ||||
| 
 | ||||
|     private static String CapacitorFileScheme = Bridge.CAPACITOR_FILE_START; | ||||
| 
 | ||||
|     public enum Type { | ||||
|         IMAGE("image"); | ||||
| 
 | ||||
|         private String type; | ||||
| 
 | ||||
|         Type(String type) { | ||||
|             this.type = type; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static String getPortablePath(Context c, String host, Uri u) { | ||||
|         String path = getFileUrlForUri(c, u); | ||||
|         if (path.startsWith("file://")) { | ||||
|             path = path.replace("file://", ""); | ||||
|         } | ||||
|         return host + Bridge.CAPACITOR_FILE_START + path; | ||||
|     } | ||||
| 
 | ||||
|     public static String getFileUrlForUri(final Context context, final Uri uri) { | ||||
|         // DocumentProvider | ||||
|         if (DocumentsContract.isDocumentUri(context, uri)) { | ||||
|             // ExternalStorageProvider | ||||
|             if (isExternalStorageDocument(uri)) { | ||||
|                 final String docId = DocumentsContract.getDocumentId(uri); | ||||
|                 final String[] split = docId.split(":"); | ||||
|                 final String type = split[0]; | ||||
| 
 | ||||
|                 if ("primary".equalsIgnoreCase(type)) { | ||||
|                     return legacyPrimaryPath(split[1]); | ||||
|                 } else { | ||||
|                     final int splitIndex = docId.indexOf(':', 1); | ||||
|                     final String tag = docId.substring(0, splitIndex); | ||||
|                     final String path = docId.substring(splitIndex + 1); | ||||
| 
 | ||||
|                     String nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag); | ||||
|                     if (nonPrimaryVolume != null) { | ||||
|                         String result = nonPrimaryVolume + "/" + path; | ||||
|                         File file = new File(result); | ||||
|                         if (file.exists() && file.canRead()) { | ||||
|                             return result; | ||||
|                         } | ||||
|                         return null; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             // DownloadsProvider | ||||
|             else if (isDownloadsDocument(uri)) { | ||||
|                 final String id = DocumentsContract.getDocumentId(uri); | ||||
|                 final Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); | ||||
| 
 | ||||
|                 return getDataColumn(context, contentUri, null, null); | ||||
|             } | ||||
|             // MediaProvider | ||||
|             else if (isMediaDocument(uri)) { | ||||
|                 final String docId = DocumentsContract.getDocumentId(uri); | ||||
|                 final String[] split = docId.split(":"); | ||||
|                 final String type = split[0]; | ||||
| 
 | ||||
|                 Uri contentUri = null; | ||||
|                 if ("image".equals(type)) { | ||||
|                     contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; | ||||
|                 } else if ("video".equals(type)) { | ||||
|                     contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; | ||||
|                 } else if ("audio".equals(type)) { | ||||
|                     contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; | ||||
|                 } | ||||
| 
 | ||||
|                 final String selection = "_id=?"; | ||||
|                 final String[] selectionArgs = new String[] { split[1] }; | ||||
| 
 | ||||
|                 return getDataColumn(context, contentUri, selection, selectionArgs); | ||||
|             } | ||||
|         } | ||||
|         // MediaStore (and general) | ||||
|         else if ("content".equalsIgnoreCase(uri.getScheme())) { | ||||
|             // Return the remote address | ||||
|             if (isGooglePhotosUri(uri)) return uri.getLastPathSegment(); | ||||
|             return getDataColumn(context, uri, null, null); | ||||
|         } | ||||
|         // File | ||||
|         else if ("file".equalsIgnoreCase(uri.getScheme())) { | ||||
|             return uri.getPath(); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("deprecation") | ||||
|     private static String legacyPrimaryPath(String pathPart) { | ||||
|         return Environment.getExternalStorageDirectory() + "/" + pathPart; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read a plaintext file from the assets directory. | ||||
|      * | ||||
|      * @param assetManager Used to open the file. | ||||
|      * @param fileName The path of the file to read. | ||||
|      * @return The contents of the file path. | ||||
|      * @throws IOException Thrown if any issues reading the provided file path. | ||||
|      */ | ||||
|     static String readFileFromAssets(AssetManager assetManager, String fileName) throws IOException { | ||||
|         try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open(fileName)))) { | ||||
|             StringBuilder buffer = new StringBuilder(); | ||||
|             String line; | ||||
|             while ((line = reader.readLine()) != null) { | ||||
|                 buffer.append(line).append("\n"); | ||||
|             } | ||||
| 
 | ||||
|             return buffer.toString(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read a plaintext file from within the app disk space. | ||||
|      * | ||||
|      * @param file The file to read. | ||||
|      * @return The contents of the file path. | ||||
|      * @throws IOException Thrown if any issues reading the provided file path. | ||||
|      */ | ||||
|     static String readFileFromDisk(File file) throws IOException { | ||||
|         try (BufferedReader reader = new BufferedReader(new FileReader(file))) { | ||||
|             StringBuilder buffer = new StringBuilder(); | ||||
|             String line; | ||||
|             while ((line = reader.readLine()) != null) { | ||||
|                 buffer.append(line).append("\n"); | ||||
|             } | ||||
| 
 | ||||
|             return buffer.toString(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the value of the data column for this Uri. This is useful for | ||||
|      * MediaStore Uris, and other file-based ContentProviders. | ||||
|      * | ||||
|      * @param context The context. | ||||
|      * @param uri The Uri to query. | ||||
|      * @param selection (Optional) Filter used in the query. | ||||
|      * @param selectionArgs (Optional) Selection arguments used in the query. | ||||
|      * @return The value of the _data column, which is typically a file path. | ||||
|      */ | ||||
|     private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) { | ||||
|         String path = null; | ||||
|         Cursor cursor = null; | ||||
|         final String column = "_data"; | ||||
|         final String[] projection = { column }; | ||||
| 
 | ||||
|         try { | ||||
|             cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null); | ||||
|             if (cursor != null && cursor.moveToFirst()) { | ||||
|                 final int index = cursor.getColumnIndexOrThrow(column); | ||||
|                 path = cursor.getString(index); | ||||
|             } | ||||
|         } catch (IllegalArgumentException ex) { | ||||
|             return getCopyFilePath(uri, context); | ||||
|         } finally { | ||||
|             if (cursor != null) cursor.close(); | ||||
|         } | ||||
|         if (path == null) { | ||||
|             return getCopyFilePath(uri, context); | ||||
|         } | ||||
|         return path; | ||||
|     } | ||||
| 
 | ||||
|     private static String getCopyFilePath(Uri uri, Context context) { | ||||
|         Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); | ||||
|         int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); | ||||
|         cursor.moveToFirst(); | ||||
|         String name = (cursor.getString(nameIndex)); | ||||
|         File file = new File(context.getFilesDir(), name); | ||||
|         try { | ||||
|             InputStream inputStream = context.getContentResolver().openInputStream(uri); | ||||
|             FileOutputStream outputStream = new FileOutputStream(file); | ||||
|             int read = 0; | ||||
|             int maxBufferSize = 1024 * 1024; | ||||
|             int bufferSize = Math.min(inputStream.available(), maxBufferSize); | ||||
|             final byte[] buffers = new byte[bufferSize]; | ||||
|             while ((read = inputStream.read(buffers)) != -1) { | ||||
|                 outputStream.write(buffers, 0, read); | ||||
|             } | ||||
|             inputStream.close(); | ||||
|             outputStream.close(); | ||||
|         } catch (Exception e) { | ||||
|             return null; | ||||
|         } finally { | ||||
|             if (cursor != null) cursor.close(); | ||||
|         } | ||||
|         return file.getPath(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param uri The Uri to check. | ||||
|      * @return Whether the Uri authority is ExternalStorageProvider. | ||||
|      */ | ||||
|     private static boolean isExternalStorageDocument(Uri uri) { | ||||
|         return "com.android.externalstorage.documents".equals(uri.getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param uri The Uri to check. | ||||
|      * @return Whether the Uri authority is DownloadsProvider. | ||||
|      */ | ||||
|     private static boolean isDownloadsDocument(Uri uri) { | ||||
|         return "com.android.providers.downloads.documents".equals(uri.getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param uri The Uri to check. | ||||
|      * @return Whether the Uri authority is MediaProvider. | ||||
|      */ | ||||
|     private static boolean isMediaDocument(Uri uri) { | ||||
|         return "com.android.providers.media.documents".equals(uri.getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param uri The Uri to check. | ||||
|      * @return Whether the Uri authority is Google Photos. | ||||
|      */ | ||||
|     private static boolean isGooglePhotosUri(Uri uri) { | ||||
|         return "com.google.android.apps.photos.content".equals(uri.getAuthority()); | ||||
|     } | ||||
| 
 | ||||
|     private static String getPathToNonPrimaryVolume(Context context, String tag) { | ||||
|         File[] volumes = context.getExternalCacheDirs(); | ||||
|         if (volumes != null) { | ||||
|             for (File volume : volumes) { | ||||
|                 if (volume != null) { | ||||
|                     String path = volume.getAbsolutePath(); | ||||
|                     if (path != null) { | ||||
|                         int index = path.indexOf(tag); | ||||
|                         if (index != -1) { | ||||
|                             return path.substring(0, index) + tag; | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| class InvalidPluginException extends Exception { | ||||
| 
 | ||||
|     public InvalidPluginException(String s) { | ||||
|         super(s); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| class InvalidPluginMethodException extends Exception { | ||||
| 
 | ||||
|     public InvalidPluginMethodException(String s) { | ||||
|         super(s); | ||||
|     } | ||||
| 
 | ||||
|     public InvalidPluginMethodException(Throwable t) { | ||||
|         super(t); | ||||
|     } | ||||
| 
 | ||||
|     public InvalidPluginMethodException(String s, Throwable t) { | ||||
|         super(s, t); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,51 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| 
 | ||||
| public class JSArray extends JSONArray { | ||||
| 
 | ||||
|     public JSArray() { | ||||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     public JSArray(String json) throws JSONException { | ||||
|         super(json); | ||||
|     } | ||||
| 
 | ||||
|     public JSArray(Collection copyFrom) { | ||||
|         super(copyFrom); | ||||
|     } | ||||
| 
 | ||||
|     public JSArray(Object array) throws JSONException { | ||||
|         super(array); | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("unchecked") | ||||
|     public <E> List<E> toList() throws JSONException { | ||||
|         List<E> items = new ArrayList<>(); | ||||
|         Object o = null; | ||||
|         for (int i = 0; i < this.length(); i++) { | ||||
|             o = this.get(i); | ||||
|             try { | ||||
|                 items.add((E) this.get(i)); | ||||
|             } catch (Exception ex) { | ||||
|                 throw new JSONException("Not all items are instances of the given type"); | ||||
|             } | ||||
|         } | ||||
|         return items; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new JSArray without throwing a error | ||||
|      */ | ||||
|     public static JSArray from(Object array) { | ||||
|         try { | ||||
|             return new JSArray(array); | ||||
|         } catch (JSONException ex) {} | ||||
|         return null; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,193 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import static com.getcapacitor.FileUtils.readFileFromAssets; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.text.TextUtils; | ||||
| import java.io.IOException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collection; | ||||
| import java.util.List; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| public class JSExport { | ||||
| 
 | ||||
|     private static String CATCHALL_OPTIONS_PARAM = "_options"; | ||||
|     private static String CALLBACK_PARAM = "_callback"; | ||||
| 
 | ||||
|     public static String getGlobalJS(Context context, boolean loggingEnabled, boolean isDebug) { | ||||
|         return "window.Capacitor = { DEBUG: " + isDebug + ", isLoggingEnabled: " + loggingEnabled + ", Plugins: {} };"; | ||||
|     } | ||||
| 
 | ||||
|     public static String getCordovaJS(Context context) { | ||||
|         String fileContent = ""; | ||||
|         try { | ||||
|             fileContent = readFileFromAssets(context.getAssets(), "public/cordova.js"); | ||||
|         } catch (IOException ex) { | ||||
|             Logger.error("Unable to read public/cordova.js file, Cordova plugins will not work"); | ||||
|         } | ||||
|         return fileContent; | ||||
|     } | ||||
| 
 | ||||
|     public static String getCordovaPluginsFileJS(Context context) { | ||||
|         String fileContent = ""; | ||||
|         try { | ||||
|             fileContent = readFileFromAssets(context.getAssets(), "public/cordova_plugins.js"); | ||||
|         } catch (IOException ex) { | ||||
|             Logger.error("Unable to read public/cordova_plugins.js file, Cordova plugins will not work"); | ||||
|         } | ||||
|         return fileContent; | ||||
|     } | ||||
| 
 | ||||
|     public static String getPluginJS(Collection<PluginHandle> plugins) { | ||||
|         List<String> lines = new ArrayList<>(); | ||||
|         JSONArray pluginArray = new JSONArray(); | ||||
| 
 | ||||
|         lines.add("// Begin: Capacitor Plugin JS"); | ||||
|         for (PluginHandle plugin : plugins) { | ||||
|             lines.add( | ||||
|                 "(function(w) {\n" + | ||||
|                 "var a = (w.Capacitor = w.Capacitor || {});\n" + | ||||
|                 "var p = (a.Plugins = a.Plugins || {});\n" + | ||||
|                 "var t = (p['" + | ||||
|                 plugin.getId() + | ||||
|                 "'] = {});\n" + | ||||
|                 "t.addListener = function(eventName, callback) {\n" + | ||||
|                 "  return w.Capacitor.addListener('" + | ||||
|                 plugin.getId() + | ||||
|                 "', eventName, callback);\n" + | ||||
|                 "}" | ||||
|             ); | ||||
|             Collection<PluginMethodHandle> methods = plugin.getMethods(); | ||||
|             for (PluginMethodHandle method : methods) { | ||||
|                 if (method.getName().equals("addListener") || method.getName().equals("removeListener")) { | ||||
|                     // Don't export add/remove listener, we do that automatically above as they are "special snowflakes" | ||||
|                     continue; | ||||
|                 } | ||||
|                 lines.add(generateMethodJS(plugin, method)); | ||||
|             } | ||||
| 
 | ||||
|             lines.add("})(window);\n"); | ||||
|             pluginArray.put(createPluginHeader(plugin)); | ||||
|         } | ||||
| 
 | ||||
|         return TextUtils.join("\n", lines) + "\nwindow.Capacitor.PluginHeaders = " + pluginArray.toString() + ";"; | ||||
|     } | ||||
| 
 | ||||
|     public static String getCordovaPluginJS(Context context) { | ||||
|         return getFilesContent(context, "public/plugins"); | ||||
|     } | ||||
| 
 | ||||
|     public static String getFilesContent(Context context, String path) { | ||||
|         StringBuilder builder = new StringBuilder(); | ||||
|         try { | ||||
|             String[] content = context.getAssets().list(path); | ||||
|             if (content.length > 0) { | ||||
|                 for (String file : content) { | ||||
|                     if (!file.endsWith(".map")) { | ||||
|                         builder.append(getFilesContent(context, path + "/" + file)); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 return readFileFromAssets(context.getAssets(), path); | ||||
|             } | ||||
|         } catch (IOException ex) { | ||||
|             Logger.warn("Unable to read file at path " + path); | ||||
|         } | ||||
|         return builder.toString(); | ||||
|     } | ||||
| 
 | ||||
|     private static JSONObject createPluginHeader(PluginHandle plugin) { | ||||
|         JSONObject pluginObj = new JSONObject(); | ||||
|         Collection<PluginMethodHandle> methods = plugin.getMethods(); | ||||
|         try { | ||||
|             String id = plugin.getId(); | ||||
|             JSONArray methodArray = new JSONArray(); | ||||
|             pluginObj.put("name", id); | ||||
| 
 | ||||
|             for (PluginMethodHandle method : methods) { | ||||
|                 methodArray.put(createPluginMethodHeader(method)); | ||||
|             } | ||||
| 
 | ||||
|             pluginObj.put("methods", methodArray); | ||||
|         } catch (JSONException e) { | ||||
|             // ignore | ||||
|         } | ||||
|         return pluginObj; | ||||
|     } | ||||
| 
 | ||||
|     private static JSONObject createPluginMethodHeader(PluginMethodHandle method) { | ||||
|         JSONObject methodObj = new JSONObject(); | ||||
| 
 | ||||
|         try { | ||||
|             methodObj.put("name", method.getName()); | ||||
|             if (!method.getReturnType().equals(PluginMethod.RETURN_NONE)) { | ||||
|                 methodObj.put("rtype", method.getReturnType()); | ||||
|             } | ||||
|         } catch (JSONException e) { | ||||
|             // ignore | ||||
|         } | ||||
| 
 | ||||
|         return methodObj; | ||||
|     } | ||||
| 
 | ||||
|     public static String getBridgeJS(Context context) throws JSExportException { | ||||
|         return getFilesContent(context, "native-bridge.js"); | ||||
|     } | ||||
| 
 | ||||
|     private static String generateMethodJS(PluginHandle plugin, PluginMethodHandle method) { | ||||
|         List<String> lines = new ArrayList<>(); | ||||
| 
 | ||||
|         List<String> args = new ArrayList<>(); | ||||
|         // Add the catch all param that will take a full javascript object to pass to the plugin | ||||
|         args.add(CATCHALL_OPTIONS_PARAM); | ||||
| 
 | ||||
|         String returnType = method.getReturnType(); | ||||
|         if (returnType.equals(PluginMethod.RETURN_CALLBACK)) { | ||||
|             args.add(CALLBACK_PARAM); | ||||
|         } | ||||
| 
 | ||||
|         // Create the method function declaration | ||||
|         lines.add("t['" + method.getName() + "'] = function(" + TextUtils.join(", ", args) + ") {"); | ||||
| 
 | ||||
|         switch (returnType) { | ||||
|             case PluginMethod.RETURN_NONE: | ||||
|                 lines.add( | ||||
|                     "return w.Capacitor.nativeCallback('" + | ||||
|                     plugin.getId() + | ||||
|                     "', '" + | ||||
|                     method.getName() + | ||||
|                     "', " + | ||||
|                     CATCHALL_OPTIONS_PARAM + | ||||
|                     ")" | ||||
|                 ); | ||||
|                 break; | ||||
|             case PluginMethod.RETURN_PROMISE: | ||||
|                 lines.add( | ||||
|                     "return w.Capacitor.nativePromise('" + plugin.getId() + "', '" + method.getName() + "', " + CATCHALL_OPTIONS_PARAM + ")" | ||||
|                 ); | ||||
|                 break; | ||||
|             case PluginMethod.RETURN_CALLBACK: | ||||
|                 lines.add( | ||||
|                     "return w.Capacitor.nativeCallback('" + | ||||
|                     plugin.getId() + | ||||
|                     "', '" + | ||||
|                     method.getName() + | ||||
|                     "', " + | ||||
|                     CATCHALL_OPTIONS_PARAM + | ||||
|                     ", " + | ||||
|                     CALLBACK_PARAM + | ||||
|                     ")" | ||||
|                 ); | ||||
|                 break; | ||||
|             default: | ||||
|             // TODO: Do something here? | ||||
|         } | ||||
| 
 | ||||
|         lines.add("}"); | ||||
| 
 | ||||
|         return TextUtils.join("\n", lines); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| public class JSExportException extends Exception { | ||||
| 
 | ||||
|     public JSExportException(String s) { | ||||
|         super(s); | ||||
|     } | ||||
| 
 | ||||
|     public JSExportException(Throwable t) { | ||||
|         super(t); | ||||
|     } | ||||
| 
 | ||||
|     public JSExportException(String s, Throwable t) { | ||||
|         super(s, t); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,107 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.InputStream; | ||||
| import java.io.InputStreamReader; | ||||
| import java.io.Reader; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| 
 | ||||
| /** | ||||
|  * JSInject is responsible for returning Capacitor's core | ||||
|  * runtime JS and any plugin JS back into HTML page responses | ||||
|  * to the client. | ||||
|  */ | ||||
| class JSInjector { | ||||
| 
 | ||||
|     private String globalJS; | ||||
|     private String bridgeJS; | ||||
|     private String pluginJS; | ||||
|     private String cordovaJS; | ||||
|     private String cordovaPluginsJS; | ||||
|     private String cordovaPluginsFileJS; | ||||
|     private String localUrlJS; | ||||
| 
 | ||||
|     public JSInjector( | ||||
|         String globalJS, | ||||
|         String bridgeJS, | ||||
|         String pluginJS, | ||||
|         String cordovaJS, | ||||
|         String cordovaPluginsJS, | ||||
|         String cordovaPluginsFileJS, | ||||
|         String localUrlJS | ||||
|     ) { | ||||
|         this.globalJS = globalJS; | ||||
|         this.bridgeJS = bridgeJS; | ||||
|         this.pluginJS = pluginJS; | ||||
|         this.cordovaJS = cordovaJS; | ||||
|         this.cordovaPluginsJS = cordovaPluginsJS; | ||||
|         this.cordovaPluginsFileJS = cordovaPluginsFileJS; | ||||
|         this.localUrlJS = localUrlJS; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Generates injectable JS content. | ||||
|      * This may be used in other forms of injecting that aren't using an InputStream. | ||||
|      * @return | ||||
|      */ | ||||
|     public String getScriptString() { | ||||
|         return ( | ||||
|             globalJS + | ||||
|             "\n\n" + | ||||
|             localUrlJS + | ||||
|             "\n\n" + | ||||
|             bridgeJS + | ||||
|             "\n\n" + | ||||
|             pluginJS + | ||||
|             "\n\n" + | ||||
|             cordovaJS + | ||||
|             "\n\n" + | ||||
|             cordovaPluginsFileJS + | ||||
|             "\n\n" + | ||||
|             cordovaPluginsJS | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given an InputStream from the web server, prepend it with | ||||
|      * our JS stream | ||||
|      * @param responseStream | ||||
|      * @return | ||||
|      */ | ||||
|     public InputStream getInjectedStream(InputStream responseStream) { | ||||
|         String js = "<script type=\"text/javascript\">" + getScriptString() + "</script>"; | ||||
|         String html = this.readAssetStream(responseStream); | ||||
| 
 | ||||
|         // Insert the js string at the position after <head> or before </head> using StringBuilder | ||||
|         StringBuilder modifiedHtml = new StringBuilder(html); | ||||
|         if (html.contains("<head>")) { | ||||
|             modifiedHtml.insert(html.indexOf("<head>") + "<head>".length(), "\n" + js + "\n"); | ||||
|             html = modifiedHtml.toString(); | ||||
|         } else if (html.contains("</head>")) { | ||||
|             modifiedHtml.insert(html.indexOf("</head>"), "\n" + js + "\n"); | ||||
|             html = modifiedHtml.toString(); | ||||
|         } else { | ||||
|             Logger.error("Unable to inject Capacitor, Plugins won't work"); | ||||
|         } | ||||
|         return new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)); | ||||
|     } | ||||
| 
 | ||||
|     private String readAssetStream(InputStream stream) { | ||||
|         try { | ||||
|             final int bufferSize = 1024; | ||||
|             final char[] buffer = new char[bufferSize]; | ||||
|             final StringBuilder out = new StringBuilder(); | ||||
|             Reader in = new InputStreamReader(stream, StandardCharsets.UTF_8); | ||||
|             for (;;) { | ||||
|                 int rsz = in.read(buffer, 0, buffer.length); | ||||
|                 if (rsz < 0) break; | ||||
|                 out.append(buffer, 0, rsz); | ||||
|             } | ||||
|             return out.toString(); | ||||
|         } catch (Exception e) { | ||||
|             Logger.error("Unable to process HTML asset file. This is a fatal error", e); | ||||
|         } | ||||
| 
 | ||||
|         return ""; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,164 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| /** | ||||
|  * A wrapper around JSONObject that isn't afraid to do simple | ||||
|  * JSON put operations without having to throw an exception | ||||
|  * for every little thing jeez | ||||
|  */ | ||||
| public class JSObject extends JSONObject { | ||||
| 
 | ||||
|     public JSObject() { | ||||
|         super(); | ||||
|     } | ||||
| 
 | ||||
|     public JSObject(String json) throws JSONException { | ||||
|         super(json); | ||||
|     } | ||||
| 
 | ||||
|     public JSObject(JSONObject obj, String[] names) throws JSONException { | ||||
|         super(obj, names); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert a pathetic JSONObject into a JSObject | ||||
|      * @param obj | ||||
|      */ | ||||
|     public static JSObject fromJSONObject(JSONObject obj) throws JSONException { | ||||
|         Iterator<String> keysIter = obj.keys(); | ||||
|         List<String> keys = new ArrayList<>(); | ||||
|         while (keysIter.hasNext()) { | ||||
|             keys.add(keysIter.next()); | ||||
|         } | ||||
| 
 | ||||
|         return new JSObject(obj, keys.toArray(new String[keys.size()])); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     @Nullable | ||||
|     public String getString(String key) { | ||||
|         return getString(key, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String getString(String key, @Nullable String defaultValue) { | ||||
|         try { | ||||
|             String value = super.getString(key); | ||||
|             if (!super.isNull(key)) { | ||||
|                 return value; | ||||
|             } | ||||
|         } catch (JSONException ex) {} | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Integer getInteger(String key) { | ||||
|         return getInteger(key, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Integer getInteger(String key, @Nullable Integer defaultValue) { | ||||
|         try { | ||||
|             return super.getInt(key); | ||||
|         } catch (JSONException e) {} | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Boolean getBoolean(String key, @Nullable Boolean defaultValue) { | ||||
|         try { | ||||
|             return super.getBoolean(key); | ||||
|         } catch (JSONException e) {} | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Fetch boolean from jsonObject | ||||
|      */ | ||||
|     @Nullable | ||||
|     public Boolean getBool(String key) { | ||||
|         return getBoolean(key, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public JSObject getJSObject(String name) { | ||||
|         try { | ||||
|             return getJSObject(name, null); | ||||
|         } catch (JSONException e) {} | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public JSObject getJSObject(String name, @Nullable JSObject defaultValue) throws JSONException { | ||||
|         try { | ||||
|             Object obj = get(name); | ||||
|             if (obj instanceof JSONObject) { | ||||
|                 Iterator<String> keysIter = ((JSONObject) obj).keys(); | ||||
|                 List<String> keys = new ArrayList<>(); | ||||
|                 while (keysIter.hasNext()) { | ||||
|                     keys.add(keysIter.next()); | ||||
|                 } | ||||
| 
 | ||||
|                 return new JSObject((JSONObject) obj, keys.toArray(new String[keys.size()])); | ||||
|             } | ||||
|         } catch (JSONException ex) {} | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public JSObject put(String key, boolean value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public JSObject put(String key, int value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public JSObject put(String key, long value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public JSObject put(String key, double value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public JSObject put(String key, Object value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public JSObject put(String key, String value) { | ||||
|         try { | ||||
|             super.put(key, value); | ||||
|         } catch (JSONException ex) {} | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public JSObject putSafe(String key, Object value) throws JSONException { | ||||
|         return (JSObject) super.put(key, value); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,65 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import org.json.JSONException; | ||||
| 
 | ||||
| /** | ||||
|  * Represents a single user-data value of any type on the capacitor PluginCall object. | ||||
|  */ | ||||
| public class JSValue { | ||||
| 
 | ||||
|     private final Object value; | ||||
| 
 | ||||
|     /** | ||||
|      * @param call The capacitor plugin call, used for accessing the value safely. | ||||
|      * @param name The name of the property to access. | ||||
|      */ | ||||
|     public JSValue(PluginCall call, String name) { | ||||
|         this.value = this.toValue(call, name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the coerced but uncasted underlying value. | ||||
|      */ | ||||
|     public Object getValue() { | ||||
|         return this.value; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return this.getValue().toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the underlying value as a JSObject, or throwing if it cannot. | ||||
|      * | ||||
|      * @throws JSONException If the underlying value is not a JSObject. | ||||
|      */ | ||||
|     public JSObject toJSObject() throws JSONException { | ||||
|         if (this.value instanceof JSObject) return (JSObject) this.value; | ||||
|         throw new JSONException("JSValue could not be coerced to JSObject."); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the underlying value as a JSArray, or throwing if it cannot. | ||||
|      * | ||||
|      * @throws JSONException If the underlying value is not a JSArray. | ||||
|      */ | ||||
|     public JSArray toJSArray() throws JSONException { | ||||
|         if (this.value instanceof JSArray) return (JSArray) this.value; | ||||
|         throw new JSONException("JSValue could not be coerced to JSArray."); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the underlying value this object represents, coercing it into a capacitor-friendly object if supported. | ||||
|      */ | ||||
|     private Object toValue(PluginCall call, String name) { | ||||
|         Object value = null; | ||||
|         value = call.getArray(name, null); | ||||
|         if (value != null) return value; | ||||
|         value = call.getObject(name, null); | ||||
|         if (value != null) return value; | ||||
|         value = call.getString(name, null); | ||||
|         if (value != null) return value; | ||||
|         return call.getData().opt(name); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,103 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| import android.util.Log; | ||||
| 
 | ||||
| public class Logger { | ||||
| 
 | ||||
|     public static final String LOG_TAG_CORE = "Capacitor"; | ||||
|     public static CapConfig config; | ||||
| 
 | ||||
|     private static Logger instance; | ||||
| 
 | ||||
|     private static Logger getInstance() { | ||||
|         if (instance == null) { | ||||
|             instance = new Logger(); | ||||
|         } | ||||
|         return instance; | ||||
|     } | ||||
| 
 | ||||
|     public static void init(CapConfig config) { | ||||
|         Logger.getInstance().loadConfig(config); | ||||
|     } | ||||
| 
 | ||||
|     private void loadConfig(CapConfig config) { | ||||
|         Logger.config = config; | ||||
|     } | ||||
| 
 | ||||
|     public static String tags(String... subtags) { | ||||
|         if (subtags != null && subtags.length > 0) { | ||||
|             return LOG_TAG_CORE + "/" + TextUtils.join("/", subtags); | ||||
|         } | ||||
| 
 | ||||
|         return LOG_TAG_CORE; | ||||
|     } | ||||
| 
 | ||||
|     public static void verbose(String message) { | ||||
|         verbose(LOG_TAG_CORE, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void verbose(String tag, String message) { | ||||
|         if (!shouldLog()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Log.v(tag, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void debug(String message) { | ||||
|         debug(LOG_TAG_CORE, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void debug(String tag, String message) { | ||||
|         if (!shouldLog()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Log.d(tag, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void info(String message) { | ||||
|         info(LOG_TAG_CORE, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void info(String tag, String message) { | ||||
|         if (!shouldLog()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Log.i(tag, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void warn(String message) { | ||||
|         warn(LOG_TAG_CORE, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void warn(String tag, String message) { | ||||
|         if (!shouldLog()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Log.w(tag, message); | ||||
|     } | ||||
| 
 | ||||
|     public static void error(String message) { | ||||
|         error(LOG_TAG_CORE, message, null); | ||||
|     } | ||||
| 
 | ||||
|     public static void error(String message, Throwable e) { | ||||
|         error(LOG_TAG_CORE, message, e); | ||||
|     } | ||||
| 
 | ||||
|     public static void error(String tag, String message, Throwable e) { | ||||
|         if (!shouldLog()) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         Log.e(tag, message, e); | ||||
|     } | ||||
| 
 | ||||
|     public static boolean shouldLog() { | ||||
|         return config == null || config.isLoggingEnabled(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,159 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.webkit.JavascriptInterface; | ||||
| import android.webkit.WebView; | ||||
| import androidx.webkit.JavaScriptReplyProxy; | ||||
| import androidx.webkit.WebViewCompat; | ||||
| import androidx.webkit.WebViewFeature; | ||||
| import org.apache.cordova.PluginManager; | ||||
| 
 | ||||
| /** | ||||
|  * MessageHandler handles messages from the WebView, dispatching them | ||||
|  * to plugins. | ||||
|  */ | ||||
| public class MessageHandler { | ||||
| 
 | ||||
|     private Bridge bridge; | ||||
|     private WebView webView; | ||||
|     private PluginManager cordovaPluginManager; | ||||
|     private JavaScriptReplyProxy javaScriptReplyProxy; | ||||
| 
 | ||||
|     public MessageHandler(Bridge bridge, WebView webView, PluginManager cordovaPluginManager) { | ||||
|         this.bridge = bridge; | ||||
|         this.webView = webView; | ||||
|         this.cordovaPluginManager = cordovaPluginManager; | ||||
| 
 | ||||
|         if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.getConfig().isUsingLegacyBridge()) { | ||||
|             WebViewCompat.WebMessageListener capListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> { | ||||
|                 if (isMainFrame) { | ||||
|                     postMessage(message.getData()); | ||||
|                     javaScriptReplyProxy = replyProxy; | ||||
|                 } else { | ||||
|                     Logger.warn("Plugin execution is allowed in Main Frame only"); | ||||
|                 } | ||||
|             }; | ||||
|             try { | ||||
|                 WebViewCompat.addWebMessageListener(webView, "androidBridge", bridge.getAllowedOriginRules(), capListener); | ||||
|             } catch (Exception ex) { | ||||
|                 webView.addJavascriptInterface(this, "androidBridge"); | ||||
|             } | ||||
|         } else { | ||||
|             webView.addJavascriptInterface(this, "androidBridge"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The main message handler that will be called from JavaScript | ||||
|      * to send a message to the native bridge. | ||||
|      * @param jsonStr | ||||
|      */ | ||||
|     @JavascriptInterface | ||||
|     @SuppressWarnings("unused") | ||||
|     public void postMessage(String jsonStr) { | ||||
|         try { | ||||
|             JSObject postData = new JSObject(jsonStr); | ||||
| 
 | ||||
|             String type = postData.getString("type"); | ||||
| 
 | ||||
|             boolean typeIsNotNull = type != null; | ||||
|             boolean isCordovaPlugin = typeIsNotNull && type.equals("cordova"); | ||||
|             boolean isJavaScriptError = typeIsNotNull && type.equals("js.error"); | ||||
| 
 | ||||
|             String callbackId = postData.getString("callbackId"); | ||||
| 
 | ||||
|             if (isCordovaPlugin) { | ||||
|                 String service = postData.getString("service"); | ||||
|                 String action = postData.getString("action"); | ||||
|                 String actionArgs = postData.getString("actionArgs"); | ||||
| 
 | ||||
|                 Logger.verbose( | ||||
|                     Logger.tags("Plugin"), | ||||
|                     "To native (Cordova plugin): callbackId: " + | ||||
|                     callbackId + | ||||
|                     ", service: " + | ||||
|                     service + | ||||
|                     ", action: " + | ||||
|                     action + | ||||
|                     ", actionArgs: " + | ||||
|                     actionArgs | ||||
|                 ); | ||||
| 
 | ||||
|                 this.callCordovaPluginMethod(callbackId, service, action, actionArgs); | ||||
|             } else if (isJavaScriptError) { | ||||
|                 Logger.error("JavaScript Error: " + jsonStr); | ||||
|             } else { | ||||
|                 String pluginId = postData.getString("pluginId"); | ||||
|                 String methodName = postData.getString("methodName"); | ||||
|                 JSObject methodData = postData.getJSObject("options", new JSObject()); | ||||
| 
 | ||||
|                 Logger.verbose( | ||||
|                     Logger.tags("Plugin"), | ||||
|                     "To native (Capacitor plugin): callbackId: " + callbackId + ", pluginId: " + pluginId + ", methodName: " + methodName | ||||
|                 ); | ||||
| 
 | ||||
|                 this.callPluginMethod(callbackId, pluginId, methodName, methodData); | ||||
|             } | ||||
|         } catch (Exception ex) { | ||||
|             Logger.error("Post message error:", ex); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public void sendResponseMessage(PluginCall call, PluginResult successResult, PluginResult errorResult) { | ||||
|         try { | ||||
|             PluginResult data = new PluginResult(); | ||||
|             data.put("save", call.isKeptAlive()); | ||||
|             data.put("callbackId", call.getCallbackId()); | ||||
|             data.put("pluginId", call.getPluginId()); | ||||
|             data.put("methodName", call.getMethodName()); | ||||
| 
 | ||||
|             boolean pluginResultInError = errorResult != null; | ||||
|             if (pluginResultInError) { | ||||
|                 data.put("success", false); | ||||
|                 data.put("error", errorResult); | ||||
|                 Logger.debug("Sending plugin error: " + data.toString()); | ||||
|             } else { | ||||
|                 data.put("success", true); | ||||
|                 if (successResult != null) { | ||||
|                     data.put("data", successResult); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             boolean isValidCallbackId = !call.getCallbackId().equals(PluginCall.CALLBACK_ID_DANGLING); | ||||
|             if (isValidCallbackId) { | ||||
|                 if (bridge.getConfig().isUsingLegacyBridge()) { | ||||
|                     legacySendResponseMessage(data); | ||||
|                 } else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) { | ||||
|                     javaScriptReplyProxy.postMessage(data.toString()); | ||||
|                 } else { | ||||
|                     legacySendResponseMessage(data); | ||||
|                 } | ||||
|             } else { | ||||
|                 bridge.getApp().fireRestoredResult(data); | ||||
|             } | ||||
|         } catch (Exception ex) { | ||||
|             Logger.error("sendResponseMessage: error: " + ex); | ||||
|         } | ||||
|         if (!call.isKeptAlive()) { | ||||
|             call.release(bridge); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void legacySendResponseMessage(PluginResult data) { | ||||
|         final String runScript = "window.Capacitor.fromNative(" + data.toString() + ")"; | ||||
|         final WebView webView = this.webView; | ||||
|         webView.post(() -> webView.evaluateJavascript(runScript, null)); | ||||
|     } | ||||
| 
 | ||||
|     private void callPluginMethod(String callbackId, String pluginId, String methodName, JSObject methodData) { | ||||
|         PluginCall call = new PluginCall(this, pluginId, callbackId, methodName, methodData); | ||||
|         bridge.callPluginMethod(pluginId, methodName, call); | ||||
|     } | ||||
| 
 | ||||
|     private void callCordovaPluginMethod(String callbackId, String service, String action, String actionArgs) { | ||||
|         bridge.execute( | ||||
|             () -> { | ||||
|                 cordovaPluginManager.exec(service, action, callbackId, actionArgs); | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import com.getcapacitor.annotation.CapacitorPlugin; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| /** | ||||
|  * Base annotation for all Plugins | ||||
|  * @deprecated | ||||
|  * <p> Use {@link CapacitorPlugin} instead | ||||
|  */ | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Deprecated | ||||
| public @interface NativePlugin { | ||||
|     /** | ||||
|      * Request codes this plugin uses and responds to, in order to tie | ||||
|      * Android events back the plugin to handle | ||||
|      */ | ||||
|     int[] requestCodes() default {}; | ||||
| 
 | ||||
|     /** | ||||
|      * Permissions this plugin needs, in order to make permission requests | ||||
|      * easy if the plugin only needs basic permission prompting | ||||
|      */ | ||||
|     String[] permissions() default {}; | ||||
| 
 | ||||
|     /** | ||||
|      * The request code to use when automatically requesting permissions | ||||
|      */ | ||||
|     int permissionRequestCode() default 9000; | ||||
| 
 | ||||
|     /** | ||||
|      * A custom name for the plugin, otherwise uses the | ||||
|      * simple class name. | ||||
|      */ | ||||
|     String name() default ""; | ||||
| } | ||||
|  | @ -0,0 +1,31 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.util.Locale; | ||||
| 
 | ||||
| /** | ||||
|  * Represents the state of a permission | ||||
|  * | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public enum PermissionState { | ||||
|     GRANTED("granted"), | ||||
|     DENIED("denied"), | ||||
|     PROMPT("prompt"), | ||||
|     PROMPT_WITH_RATIONALE("prompt-with-rationale"); | ||||
| 
 | ||||
|     private String state; | ||||
| 
 | ||||
|     PermissionState(String state) { | ||||
|         this.state = state; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return state; | ||||
|     } | ||||
| 
 | ||||
|     public static PermissionState byState(String state) { | ||||
|         state = state.toUpperCase(Locale.ROOT).replace('-', '_'); | ||||
|         return valueOf(state); | ||||
|     } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							|  | @ -0,0 +1,440 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import androidx.annotation.Nullable; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| /** | ||||
|  * Wraps a call from the web layer to native | ||||
|  */ | ||||
| public class PluginCall { | ||||
| 
 | ||||
|     /** | ||||
|      * A special callback id that indicates there is no matching callback | ||||
|      * on the client to associate any PluginCall results back to. This is used | ||||
|      * in the case of an app resuming with saved instance data, for example. | ||||
|      */ | ||||
|     public static final String CALLBACK_ID_DANGLING = "-1"; | ||||
| 
 | ||||
|     private final MessageHandler msgHandler; | ||||
|     private final String pluginId; | ||||
|     private final String callbackId; | ||||
|     private final String methodName; | ||||
|     private final JSObject data; | ||||
| 
 | ||||
|     private boolean keepAlive = false; | ||||
| 
 | ||||
|     /** | ||||
|      * Indicates that this PluginCall was released, and should no longer be used | ||||
|      */ | ||||
|     @Deprecated | ||||
|     private boolean isReleased = false; | ||||
| 
 | ||||
|     public PluginCall(MessageHandler msgHandler, String pluginId, String callbackId, String methodName, JSObject data) { | ||||
|         this.msgHandler = msgHandler; | ||||
|         this.pluginId = pluginId; | ||||
|         this.callbackId = callbackId; | ||||
|         this.methodName = methodName; | ||||
|         this.data = data; | ||||
|     } | ||||
| 
 | ||||
|     public void successCallback(PluginResult successResult) { | ||||
|         if (CALLBACK_ID_DANGLING.equals(this.callbackId)) { | ||||
|             // don't send back response if the callbackId was "-1" | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         this.msgHandler.sendResponseMessage(this, successResult, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated | ||||
|      * Use {@link #resolve(JSObject data)} | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void success(JSObject data) { | ||||
|         PluginResult result = new PluginResult(data); | ||||
|         this.msgHandler.sendResponseMessage(this, result, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated | ||||
|      * Use {@link #resolve()} | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void success() { | ||||
|         this.resolve(new JSObject()); | ||||
|     } | ||||
| 
 | ||||
|     public void resolve(JSObject data) { | ||||
|         PluginResult result = new PluginResult(data); | ||||
|         this.msgHandler.sendResponseMessage(this, result, null); | ||||
|     } | ||||
| 
 | ||||
|     public void resolve() { | ||||
|         this.msgHandler.sendResponseMessage(this, null, null); | ||||
|     } | ||||
| 
 | ||||
|     public void errorCallback(String msg) { | ||||
|         PluginResult errorResult = new PluginResult(); | ||||
| 
 | ||||
|         try { | ||||
|             errorResult.put("message", msg); | ||||
|         } catch (Exception jsonEx) { | ||||
|             Logger.error(Logger.tags("Plugin"), jsonEx.toString(), null); | ||||
|         } | ||||
| 
 | ||||
|         this.msgHandler.sendResponseMessage(this, null, errorResult); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated | ||||
|      * Use {@link #reject(String msg, Exception ex)} | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void error(String msg, Exception ex) { | ||||
|         reject(msg, ex); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated | ||||
|      * Use {@link #reject(String msg, String code, Exception ex)} | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void error(String msg, String code, Exception ex) { | ||||
|         reject(msg, code, ex); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated | ||||
|      * Use {@link #reject(String msg)} | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void error(String msg) { | ||||
|         reject(msg); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, String code, Exception ex, JSObject data) { | ||||
|         PluginResult errorResult = new PluginResult(); | ||||
| 
 | ||||
|         if (ex != null) { | ||||
|             Logger.error(Logger.tags("Plugin"), msg, ex); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             errorResult.put("message", msg); | ||||
|             errorResult.put("code", code); | ||||
|             if (null != data) { | ||||
|                 errorResult.put("data", data); | ||||
|             } | ||||
|         } catch (Exception jsonEx) { | ||||
|             Logger.error(Logger.tags("Plugin"), jsonEx.getMessage(), jsonEx); | ||||
|         } | ||||
| 
 | ||||
|         this.msgHandler.sendResponseMessage(this, null, errorResult); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, Exception ex, JSObject data) { | ||||
|         reject(msg, null, ex, data); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, String code, JSObject data) { | ||||
|         reject(msg, code, null, data); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, String code, Exception ex) { | ||||
|         reject(msg, code, ex, null); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, JSObject data) { | ||||
|         reject(msg, null, null, data); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, Exception ex) { | ||||
|         reject(msg, null, ex, null); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg, String code) { | ||||
|         reject(msg, code, null, null); | ||||
|     } | ||||
| 
 | ||||
|     public void reject(String msg) { | ||||
|         reject(msg, null, null, null); | ||||
|     } | ||||
| 
 | ||||
|     public void unimplemented() { | ||||
|         unimplemented("not implemented"); | ||||
|     } | ||||
| 
 | ||||
|     public void unimplemented(String msg) { | ||||
|         reject(msg, "UNIMPLEMENTED", null, null); | ||||
|     } | ||||
| 
 | ||||
|     public void unavailable() { | ||||
|         unavailable("not available"); | ||||
|     } | ||||
| 
 | ||||
|     public void unavailable(String msg) { | ||||
|         reject(msg, "UNAVAILABLE", null, null); | ||||
|     } | ||||
| 
 | ||||
|     public String getPluginId() { | ||||
|         return this.pluginId; | ||||
|     } | ||||
| 
 | ||||
|     public String getCallbackId() { | ||||
|         return this.callbackId; | ||||
|     } | ||||
| 
 | ||||
|     public String getMethodName() { | ||||
|         return this.methodName; | ||||
|     } | ||||
| 
 | ||||
|     public JSObject getData() { | ||||
|         return this.data; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String getString(String name) { | ||||
|         return this.getString(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public String getString(String name, @Nullable String defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof String) { | ||||
|             return (String) value; | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Integer getInt(String name) { | ||||
|         return this.getInt(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Integer getInt(String name, @Nullable Integer defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof Integer) { | ||||
|             return (Integer) value; | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Long getLong(String name) { | ||||
|         return this.getLong(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Long getLong(String name, @Nullable Long defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof Long) { | ||||
|             return (Long) value; | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Float getFloat(String name) { | ||||
|         return this.getFloat(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Float getFloat(String name, @Nullable Float defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof Float) { | ||||
|             return (Float) value; | ||||
|         } | ||||
|         if (value instanceof Double) { | ||||
|             return ((Double) value).floatValue(); | ||||
|         } | ||||
|         if (value instanceof Integer) { | ||||
|             return ((Integer) value).floatValue(); | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Double getDouble(String name) { | ||||
|         return this.getDouble(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Double getDouble(String name, @Nullable Double defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof Double) { | ||||
|             return (Double) value; | ||||
|         } | ||||
|         if (value instanceof Float) { | ||||
|             return ((Float) value).doubleValue(); | ||||
|         } | ||||
|         if (value instanceof Integer) { | ||||
|             return ((Integer) value).doubleValue(); | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Boolean getBoolean(String name) { | ||||
|         return this.getBoolean(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public Boolean getBoolean(String name, @Nullable Boolean defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof Boolean) { | ||||
|             return (Boolean) value; | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     public JSObject getObject(String name) { | ||||
|         return this.getObject(name, null); | ||||
|     } | ||||
| 
 | ||||
|     @Nullable | ||||
|     public JSObject getObject(String name, JSObject defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof JSONObject) { | ||||
|             try { | ||||
|                 return JSObject.fromJSONObject((JSONObject) value); | ||||
|             } catch (JSONException ex) { | ||||
|                 return defaultValue; | ||||
|             } | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     public JSArray getArray(String name) { | ||||
|         return this.getArray(name, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a JSONArray and turn it into a JSArray | ||||
|      * @param name | ||||
|      * @param defaultValue | ||||
|      * @return | ||||
|      */ | ||||
|     @Nullable | ||||
|     public JSArray getArray(String name, JSArray defaultValue) { | ||||
|         Object value = this.data.opt(name); | ||||
|         if (value == null) { | ||||
|             return defaultValue; | ||||
|         } | ||||
| 
 | ||||
|         if (value instanceof JSONArray) { | ||||
|             try { | ||||
|                 JSONArray valueArray = (JSONArray) value; | ||||
|                 List<Object> items = new ArrayList<>(); | ||||
|                 for (int i = 0; i < valueArray.length(); i++) { | ||||
|                     items.add(valueArray.get(i)); | ||||
|                 } | ||||
|                 return new JSArray(items.toArray()); | ||||
|             } catch (JSONException ex) { | ||||
|                 return defaultValue; | ||||
|             } | ||||
|         } | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @param name of the option to check | ||||
|      * @return boolean indicating if the plugin call has an option for the provided name. | ||||
|      * @deprecated Presence of a key should not be considered significant. | ||||
|      * Use typed accessors to check the value instead. | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public boolean hasOption(String name) { | ||||
|         return this.data.has(name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Indicate that the Bridge should cache this call in order to call | ||||
|      * it again later. For example, the addListener system uses this to | ||||
|      * continuously call the call's callback (😆). | ||||
|      * @deprecated use {@link #setKeepAlive(Boolean)} instead | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public void save() { | ||||
|         setKeepAlive(true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Indicate that the Bridge should cache this call in order to call | ||||
|      * it again later. For example, the addListener system uses this to | ||||
|      * continuously call the call's callback. | ||||
|      * | ||||
|      * @param keepAlive whether to keep the callback saved | ||||
|      */ | ||||
|     public void setKeepAlive(Boolean keepAlive) { | ||||
|         this.keepAlive = keepAlive; | ||||
|     } | ||||
| 
 | ||||
|     public void release(Bridge bridge) { | ||||
|         this.keepAlive = false; | ||||
|         bridge.releaseCall(this); | ||||
|         this.isReleased = true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated use {@link #isKeptAlive()} | ||||
|      * @return true if the plugin call is kept alive | ||||
|      */ | ||||
|     @Deprecated | ||||
|     public boolean isSaved() { | ||||
|         return isKeptAlive(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the keepAlive value of the plugin call | ||||
|      * @return true if the plugin call is kept alive | ||||
|      */ | ||||
|     public boolean isKeptAlive() { | ||||
|         return keepAlive; | ||||
|     } | ||||
| 
 | ||||
|     @Deprecated | ||||
|     public boolean isReleased() { | ||||
|         return isReleased; | ||||
|     } | ||||
| 
 | ||||
|     class PluginCallDataTypeException extends Exception { | ||||
| 
 | ||||
|         PluginCallDataTypeException(String m) { | ||||
|             super(m); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,116 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import com.getcapacitor.util.JSONUtils; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| /** | ||||
|  * Represents the configuration options for plugins used by Capacitor | ||||
|  */ | ||||
| public class PluginConfig { | ||||
| 
 | ||||
|     /** | ||||
|      * The object containing plugin config values. | ||||
|      */ | ||||
|     private final JSONObject config; | ||||
| 
 | ||||
|     /** | ||||
|      * Constructs a PluginsConfig with the provided JSONObject value. | ||||
|      * | ||||
|      * @param config A plugin configuration expressed as a JSON Object | ||||
|      */ | ||||
|     PluginConfig(JSONObject config) { | ||||
|         this.config = config; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     public String getString(String configKey) { | ||||
|         return getString(configKey, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     public String getString(String configKey, String defaultValue) { | ||||
|         return JSONUtils.getString(config, configKey, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a boolean value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     public boolean getBoolean(String configKey, boolean defaultValue) { | ||||
|         return JSONUtils.getBoolean(config, configKey, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an integer value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     public int getInt(String configKey, int defaultValue) { | ||||
|         return JSONUtils.getInt(config, configKey, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string array value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     public String[] getArray(String configKey) { | ||||
|         return getArray(configKey, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string array value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @param defaultValue A default value to return if the key does not exist in the config | ||||
|      * @return The value from the config, if key exists. Default value returned if not | ||||
|      */ | ||||
|     public String[] getArray(String configKey, String[] defaultValue) { | ||||
|         return JSONUtils.getArray(config, configKey, defaultValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a JSON object value for a plugin in the Capacitor config. | ||||
|      * | ||||
|      * @param configKey The key of the value to retrieve | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     public JSONObject getObject(String configKey) { | ||||
|         return JSONUtils.getObject(config, configKey); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if the PluginConfig is empty. | ||||
|      * | ||||
|      * @return true if the plugin config has no entries | ||||
|      */ | ||||
|     public boolean isEmpty() { | ||||
|         return config.length() == 0; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the JSON Object containing the config of the the provided plugin ID. | ||||
|      * | ||||
|      * @return The config for that plugin | ||||
|      */ | ||||
|     public JSONObject getConfigJSON() { | ||||
|         return config; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,160 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import com.getcapacitor.annotation.CapacitorPlugin; | ||||
| import java.lang.reflect.InvocationTargetException; | ||||
| import java.lang.reflect.Method; | ||||
| import java.util.Collection; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * PluginHandle is an instance of a plugin that has been registered | ||||
|  * and indexed. Think of it as a Plugin instance with extra metadata goodies | ||||
|  */ | ||||
| public class PluginHandle { | ||||
| 
 | ||||
|     private final Bridge bridge; | ||||
|     private final Class<? extends Plugin> pluginClass; | ||||
| 
 | ||||
|     private final Map<String, PluginMethodHandle> pluginMethods = new HashMap<>(); | ||||
| 
 | ||||
|     private final String pluginId; | ||||
| 
 | ||||
|     @SuppressWarnings("deprecation") | ||||
|     private NativePlugin legacyPluginAnnotation; | ||||
| 
 | ||||
|     private CapacitorPlugin pluginAnnotation; | ||||
| 
 | ||||
|     private Plugin instance; | ||||
| 
 | ||||
|     @SuppressWarnings("deprecation") | ||||
|     private PluginHandle(Class<? extends Plugin> clazz, Bridge bridge) throws InvalidPluginException { | ||||
|         this.bridge = bridge; | ||||
|         this.pluginClass = clazz; | ||||
| 
 | ||||
|         CapacitorPlugin pluginAnnotation = pluginClass.getAnnotation(CapacitorPlugin.class); | ||||
|         if (pluginAnnotation == null) { | ||||
|             // Check for legacy plugin annotation, @NativePlugin | ||||
|             NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); | ||||
|             if (legacyPluginAnnotation == null) { | ||||
|                 throw new InvalidPluginException("No @CapacitorPlugin annotation found for plugin " + pluginClass.getName()); | ||||
|             } | ||||
| 
 | ||||
|             if (!legacyPluginAnnotation.name().equals("")) { | ||||
|                 this.pluginId = legacyPluginAnnotation.name(); | ||||
|             } else { | ||||
|                 this.pluginId = pluginClass.getSimpleName(); | ||||
|             } | ||||
| 
 | ||||
|             this.legacyPluginAnnotation = legacyPluginAnnotation; | ||||
|         } else { | ||||
|             if (!pluginAnnotation.name().equals("")) { | ||||
|                 this.pluginId = pluginAnnotation.name(); | ||||
|             } else { | ||||
|                 this.pluginId = pluginClass.getSimpleName(); | ||||
|             } | ||||
| 
 | ||||
|             this.pluginAnnotation = pluginAnnotation; | ||||
|         } | ||||
| 
 | ||||
|         this.indexMethods(clazz); | ||||
|     } | ||||
| 
 | ||||
|     public PluginHandle(Bridge bridge, Class<? extends Plugin> pluginClass) throws InvalidPluginException, PluginLoadException { | ||||
|         this(pluginClass, bridge); | ||||
|         this.load(); | ||||
|     } | ||||
| 
 | ||||
|     public PluginHandle(Bridge bridge, Plugin plugin) throws InvalidPluginException { | ||||
|         this(plugin.getClass(), bridge); | ||||
|         this.loadInstance(plugin); | ||||
|     } | ||||
| 
 | ||||
|     public Class<? extends Plugin> getPluginClass() { | ||||
|         return pluginClass; | ||||
|     } | ||||
| 
 | ||||
|     public String getId() { | ||||
|         return this.pluginId; | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("deprecation") | ||||
|     public NativePlugin getLegacyPluginAnnotation() { | ||||
|         return this.legacyPluginAnnotation; | ||||
|     } | ||||
| 
 | ||||
|     public CapacitorPlugin getPluginAnnotation() { | ||||
|         return this.pluginAnnotation; | ||||
|     } | ||||
| 
 | ||||
|     public Plugin getInstance() { | ||||
|         return this.instance; | ||||
|     } | ||||
| 
 | ||||
|     public Collection<PluginMethodHandle> getMethods() { | ||||
|         return this.pluginMethods.values(); | ||||
|     } | ||||
| 
 | ||||
|     public Plugin load() throws PluginLoadException { | ||||
|         if (this.instance != null) { | ||||
|             return this.instance; | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             this.instance = this.pluginClass.getDeclaredConstructor().newInstance(); | ||||
|             return this.loadInstance(instance); | ||||
|         } catch (Exception ex) { | ||||
|             throw new PluginLoadException("Unable to load plugin instance. Ensure plugin is publicly accessible"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public Plugin loadInstance(Plugin plugin) { | ||||
|         this.instance = plugin; | ||||
|         this.instance.setPluginHandle(this); | ||||
|         this.instance.setBridge(this.bridge); | ||||
|         this.instance.load(); | ||||
|         this.instance.initializeActivityLaunchers(); | ||||
|         return this.instance; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Call a method on a plugin. | ||||
|      * @param methodName the name of the method to call | ||||
|      * @param call the constructed PluginCall with parameters from the caller | ||||
|      * @throws InvalidPluginMethodException if no method was found on that plugin | ||||
|      */ | ||||
|     public void invoke(String methodName, PluginCall call) | ||||
|         throws PluginLoadException, InvalidPluginMethodException, InvocationTargetException, IllegalAccessException { | ||||
|         if (this.instance == null) { | ||||
|             // Can throw PluginLoadException | ||||
|             this.load(); | ||||
|         } | ||||
| 
 | ||||
|         PluginMethodHandle methodMeta = pluginMethods.get(methodName); | ||||
|         if (methodMeta == null) { | ||||
|             throw new InvalidPluginMethodException("No method " + methodName + " found for plugin " + pluginClass.getName()); | ||||
|         } | ||||
| 
 | ||||
|         methodMeta.getMethod().invoke(this.instance, call); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Index all the known callable methods for a plugin for faster | ||||
|      * invocation later | ||||
|      */ | ||||
|     private void indexMethods(Class<? extends Plugin> plugin) { | ||||
|         //Method[] methods = pluginClass.getDeclaredMethods(); | ||||
|         Method[] methods = pluginClass.getMethods(); | ||||
| 
 | ||||
|         for (Method methodReflect : methods) { | ||||
|             PluginMethod method = methodReflect.getAnnotation(PluginMethod.class); | ||||
| 
 | ||||
|             if (method == null) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             PluginMethodHandle methodMeta = new PluginMethodHandle(methodReflect, method); | ||||
|             pluginMethods.put(methodReflect.getName(), methodMeta); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,16 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| class PluginInvocationException extends Exception { | ||||
| 
 | ||||
|     public PluginInvocationException(String s) { | ||||
|         super(s); | ||||
|     } | ||||
| 
 | ||||
|     public PluginInvocationException(Throwable t) { | ||||
|         super(t); | ||||
|     } | ||||
| 
 | ||||
|     public PluginInvocationException(String s, Throwable t) { | ||||
|         super(s, t); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,19 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| /** | ||||
|  * Thrown when a plugin fails to instantiate | ||||
|  */ | ||||
| public class PluginLoadException extends Exception { | ||||
| 
 | ||||
|     public PluginLoadException(String s) { | ||||
|         super(s); | ||||
|     } | ||||
| 
 | ||||
|     public PluginLoadException(Throwable t) { | ||||
|         super(t); | ||||
|     } | ||||
| 
 | ||||
|     public PluginLoadException(String s, Throwable t) { | ||||
|         super(s, t); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,56 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.content.res.AssetManager; | ||||
| import java.io.BufferedReader; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStreamReader; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| public class PluginManager { | ||||
| 
 | ||||
|     private final AssetManager assetManager; | ||||
| 
 | ||||
|     public PluginManager(AssetManager assetManager) { | ||||
|         this.assetManager = assetManager; | ||||
|     } | ||||
| 
 | ||||
|     public List<Class<? extends Plugin>> loadPluginClasses() throws PluginLoadException { | ||||
|         JSONArray pluginsJSON = parsePluginsJSON(); | ||||
|         ArrayList<Class<? extends Plugin>> pluginList = new ArrayList<>(); | ||||
| 
 | ||||
|         try { | ||||
|             for (int i = 0, size = pluginsJSON.length(); i < size; i++) { | ||||
|                 JSONObject pluginJSON = pluginsJSON.getJSONObject(i); | ||||
|                 String classPath = pluginJSON.getString("classpath"); | ||||
|                 Class<?> c = Class.forName(classPath); | ||||
|                 pluginList.add(c.asSubclass(Plugin.class)); | ||||
|             } | ||||
|         } catch (JSONException e) { | ||||
|             throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON"); | ||||
|         } catch (ClassNotFoundException e) { | ||||
|             throw new PluginLoadException("Could not find class by class path: " + e.getMessage()); | ||||
|         } | ||||
| 
 | ||||
|         return pluginList; | ||||
|     } | ||||
| 
 | ||||
|     private JSONArray parsePluginsJSON() throws PluginLoadException { | ||||
|         try (BufferedReader reader = new BufferedReader(new InputStreamReader(assetManager.open("capacitor.plugins.json")))) { | ||||
|             StringBuilder builder = new StringBuilder(); | ||||
|             String line; | ||||
|             while ((line = reader.readLine()) != null) { | ||||
|                 builder.append(line); | ||||
|             } | ||||
|             String jsonString = builder.toString(); | ||||
|             return new JSONArray(jsonString); | ||||
|         } catch (IOException e) { | ||||
|             throw new PluginLoadException("Could not load capacitor.plugins.json"); | ||||
|         } catch (JSONException e) { | ||||
|             throw new PluginLoadException("Could not parse capacitor.plugins.json as JSON"); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,15 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface PluginMethod { | ||||
|     String RETURN_PROMISE = "promise"; | ||||
| 
 | ||||
|     String RETURN_CALLBACK = "callback"; | ||||
| 
 | ||||
|     String RETURN_NONE = "none"; | ||||
| 
 | ||||
|     String returnType() default RETURN_PROMISE; | ||||
| } | ||||
|  | @ -0,0 +1,33 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.lang.reflect.Method; | ||||
| 
 | ||||
| public class PluginMethodHandle { | ||||
| 
 | ||||
|     // The reflect method reference | ||||
|     private final Method method; | ||||
|     // The name of the method | ||||
|     private final String name; | ||||
|     // The return type of the method (see PluginMethod for constants) | ||||
|     private final String returnType; | ||||
| 
 | ||||
|     public PluginMethodHandle(Method method, PluginMethod methodDecorator) { | ||||
|         this.method = method; | ||||
| 
 | ||||
|         this.name = method.getName(); | ||||
| 
 | ||||
|         this.returnType = methodDecorator.returnType(); | ||||
|     } | ||||
| 
 | ||||
|     public String getReturnType() { | ||||
|         return returnType; | ||||
|     } | ||||
| 
 | ||||
|     public String getName() { | ||||
|         return name; | ||||
|     } | ||||
| 
 | ||||
|     public Method getMethod() { | ||||
|         return method; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,84 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import java.text.DateFormat; | ||||
| import java.text.SimpleDateFormat; | ||||
| import java.util.Date; | ||||
| import java.util.TimeZone; | ||||
| 
 | ||||
| /** | ||||
|  * Wraps a result for web from calling a native plugin. | ||||
|  */ | ||||
| public class PluginResult { | ||||
| 
 | ||||
|     private final JSObject json; | ||||
| 
 | ||||
|     public PluginResult() { | ||||
|         this(new JSObject()); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult(JSObject json) { | ||||
|         this.json = json; | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, boolean value) { | ||||
|         return this.jsonPut(name, value); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, double value) { | ||||
|         return this.jsonPut(name, value); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, int value) { | ||||
|         return this.jsonPut(name, value); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, long value) { | ||||
|         return this.jsonPut(name, value); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Format a date as an ISO string | ||||
|      */ | ||||
|     public PluginResult put(String name, Date value) { | ||||
|         TimeZone tz = TimeZone.getTimeZone("UTC"); | ||||
|         DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'"); | ||||
|         df.setTimeZone(tz); | ||||
|         return this.jsonPut(name, df.format(value)); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, Object value) { | ||||
|         return this.jsonPut(name, value); | ||||
|     } | ||||
| 
 | ||||
|     public PluginResult put(String name, PluginResult value) { | ||||
|         return this.jsonPut(name, value.json); | ||||
|     } | ||||
| 
 | ||||
|     PluginResult jsonPut(String name, Object value) { | ||||
|         try { | ||||
|             this.json.put(name, value); | ||||
|         } catch (Exception ex) { | ||||
|             Logger.error(Logger.tags("Plugin"), "", ex); | ||||
|         } | ||||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public String toString() { | ||||
|         return this.json.toString(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Return plugin metadata and information about the result, if it succeeded the data, or error information if it didn't. | ||||
|      * This is used for appRestoredResult, as it's technically a raw data response from a plugin. | ||||
|      * @return the raw data response from the plugin. | ||||
|      */ | ||||
|     public JSObject getWrappedResult() { | ||||
|         JSObject ret = new JSObject(); | ||||
|         ret.put("pluginId", this.json.getString("pluginId")); | ||||
|         ret.put("methodName", this.json.getString("methodName")); | ||||
|         ret.put("success", this.json.getBoolean("success", false)); | ||||
|         ret.put("data", this.json.getJSObject("data")); | ||||
|         ret.put("error", this.json.getJSObject("error")); | ||||
|         return ret; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,37 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| /** | ||||
|  * An data class used in conjunction with RouteProcessor. | ||||
|  * | ||||
|  * @see com.getcapacitor.RouteProcessor | ||||
|  */ | ||||
| public class ProcessedRoute { | ||||
| 
 | ||||
|     private String path; | ||||
|     private boolean isAsset; | ||||
|     private boolean ignoreAssetPath; | ||||
| 
 | ||||
|     public String getPath() { | ||||
|         return path; | ||||
|     } | ||||
| 
 | ||||
|     public void setPath(String path) { | ||||
|         this.path = path; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isAsset() { | ||||
|         return isAsset; | ||||
|     } | ||||
| 
 | ||||
|     public void setAsset(boolean asset) { | ||||
|         isAsset = asset; | ||||
|     } | ||||
| 
 | ||||
|     public boolean isIgnoreAssetPath() { | ||||
|         return ignoreAssetPath; | ||||
|     } | ||||
| 
 | ||||
|     public void setIgnoreAssetPath(boolean ignoreAssetPath) { | ||||
|         this.ignoreAssetPath = ignoreAssetPath; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,8 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| /** | ||||
|  * An interface used in the processing of routes | ||||
|  */ | ||||
| public interface RouteProcessor { | ||||
|     ProcessedRoute process(String basePath, String path); | ||||
| } | ||||
|  | @ -0,0 +1,25 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| public class ServerPath { | ||||
| 
 | ||||
|     public enum PathType { | ||||
|         BASE_PATH, | ||||
|         ASSET_PATH | ||||
|     } | ||||
| 
 | ||||
|     private final PathType type; | ||||
|     private final String path; | ||||
| 
 | ||||
|     public ServerPath(PathType type, String path) { | ||||
|         this.type = type; | ||||
|         this.path = path; | ||||
|     } | ||||
| 
 | ||||
|     public PathType getType() { | ||||
|         return type; | ||||
|     } | ||||
| 
 | ||||
|     public String getPath() { | ||||
|         return path; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										180
									
								
								@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java
									
										
									
									
									
										Executable file
									
								
							
							
						
						
									
										180
									
								
								@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java
									
										
									
									
									
										Executable file
									
								
							|  | @ -0,0 +1,180 @@ | |||
| /* | ||||
|  * Copyright (C) 2006 The Android Open Source Project | ||||
|  * | ||||
|  * Licensed under the Apache License, Version 2.0 (the "License"); | ||||
|  * you may not use this file except in compliance with the License. | ||||
|  * You may obtain a copy of the License at | ||||
|  * | ||||
|  *      http://www.apache.org/licenses/LICENSE-2.0 | ||||
|  * | ||||
|  * Unless required by applicable law or agreed to in writing, software | ||||
|  * distributed under the License is distributed on an "AS IS" BASIS, | ||||
|  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
|  * See the License for the specific language governing permissions and | ||||
|  * limitations under the License. | ||||
|  */ | ||||
| //package com.google.webviewlocalserver.third_party.android; | ||||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.net.Uri; | ||||
| import com.getcapacitor.util.HostMask; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| public class UriMatcher { | ||||
| 
 | ||||
|     /** | ||||
|      * Creates the root node of the URI tree. | ||||
|      * | ||||
|      * @param code the code to match for the root URI | ||||
|      */ | ||||
|     public UriMatcher(Object code) { | ||||
|         mCode = code; | ||||
|         mWhich = -1; | ||||
|         mChildren = new ArrayList<>(); | ||||
|         mText = null; | ||||
|     } | ||||
| 
 | ||||
|     private UriMatcher() { | ||||
|         mCode = null; | ||||
|         mWhich = -1; | ||||
|         mChildren = new ArrayList<>(); | ||||
|         mText = null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Add a URI to match, and the code to return when this URI is | ||||
|      * matched. URI nodes may be exact match string, the token "*" | ||||
|      * that matches any text, or the token "#" that matches only | ||||
|      * numbers. | ||||
|      * <p> | ||||
|      * Starting from API level {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, | ||||
|      * this method will accept a leading slash in the path. | ||||
|      * | ||||
|      * @param authority the authority to match | ||||
|      * @param path      the path to match. * may be used as a wild card for | ||||
|      *                  any text, and # may be used as a wild card for numbers. | ||||
|      * @param code      the code that is returned when a URI is matched | ||||
|      *                  against the given components. Must be positive. | ||||
|      */ | ||||
|     public void addURI(String scheme, String authority, String path, Object code) { | ||||
|         if (code == null) { | ||||
|             throw new IllegalArgumentException("Code can't be null"); | ||||
|         } | ||||
| 
 | ||||
|         String[] tokens = null; | ||||
|         if (path != null) { | ||||
|             String newPath = path; | ||||
|             // Strip leading slash if present. | ||||
|             if (!path.isEmpty() && path.charAt(0) == '/') { | ||||
|                 newPath = path.substring(1); | ||||
|             } | ||||
|             tokens = PATH_SPLIT_PATTERN.split(newPath); | ||||
|         } | ||||
| 
 | ||||
|         int numTokens = tokens != null ? tokens.length : 0; | ||||
|         UriMatcher node = this; | ||||
|         for (int i = -2; i < numTokens; i++) { | ||||
|             String token; | ||||
|             if (i == -2) token = scheme; else if (i == -1) token = authority; else token = tokens[i]; | ||||
|             ArrayList<UriMatcher> children = node.mChildren; | ||||
|             int numChildren = children.size(); | ||||
|             UriMatcher child; | ||||
|             int j; | ||||
|             for (j = 0; j < numChildren; j++) { | ||||
|                 child = children.get(j); | ||||
|                 if (token.equals(child.mText)) { | ||||
|                     node = child; | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             if (j == numChildren) { | ||||
|                 // Child not found, create it | ||||
|                 child = new UriMatcher(); | ||||
|                 if (i == -1 && token.contains("*")) { | ||||
|                     child.mWhich = MASK; | ||||
|                 } else if (token.equals("**")) { | ||||
|                     child.mWhich = REST; | ||||
|                 } else if (token.equals("*")) { | ||||
|                     child.mWhich = TEXT; | ||||
|                 } else { | ||||
|                     child.mWhich = EXACT; | ||||
|                 } | ||||
|                 child.mText = token; | ||||
|                 node.mChildren.add(child); | ||||
|                 node = child; | ||||
|             } | ||||
|         } | ||||
|         node.mCode = code; | ||||
|     } | ||||
| 
 | ||||
|     static final Pattern PATH_SPLIT_PATTERN = Pattern.compile("/"); | ||||
| 
 | ||||
|     /** | ||||
|      * Try to match against the path in a url. | ||||
|      * | ||||
|      * @param uri The url whose path we will match against. | ||||
|      * @return The code for the matched node (added using addURI), | ||||
|      * or null if there is no matched node. | ||||
|      */ | ||||
|     public Object match(Uri uri) { | ||||
|         final List<String> pathSegments = uri.getPathSegments(); | ||||
|         final int li = pathSegments.size(); | ||||
| 
 | ||||
|         UriMatcher node = this; | ||||
| 
 | ||||
|         if (li == 0 && uri.getAuthority() == null) { | ||||
|             return this.mCode; | ||||
|         } | ||||
| 
 | ||||
|         for (int i = -2; i < li; i++) { | ||||
|             String u; | ||||
|             if (i == -2) u = uri.getScheme(); else if (i == -1) u = uri.getAuthority(); else u = pathSegments.get(i); | ||||
|             ArrayList<UriMatcher> list = node.mChildren; | ||||
|             if (list == null) { | ||||
|                 break; | ||||
|             } | ||||
|             node = null; | ||||
|             int lj = list.size(); | ||||
|             for (int j = 0; j < lj; j++) { | ||||
|                 UriMatcher n = list.get(j); | ||||
|                 which_switch:switch (n.mWhich) { | ||||
|                     case MASK: | ||||
|                         if (HostMask.Parser.parse(n.mText).matches(u)) { | ||||
|                             node = n; | ||||
|                         } | ||||
|                         break; | ||||
|                     case EXACT: | ||||
|                         if (n.mText.equals(u)) { | ||||
|                             node = n; | ||||
|                         } | ||||
|                         break; | ||||
|                     case TEXT: | ||||
|                         node = n; | ||||
|                         break; | ||||
|                     case REST: | ||||
|                         return n.mCode; | ||||
|                 } | ||||
|                 if (node != null) { | ||||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             if (node == null) { | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return node.mCode; | ||||
|     } | ||||
| 
 | ||||
|     private static final int EXACT = 0; | ||||
|     private static final int TEXT = 1; | ||||
|     private static final int REST = 2; | ||||
|     private static final int MASK = 3; | ||||
| 
 | ||||
|     private Object mCode; | ||||
|     private int mWhich; | ||||
|     private String mText; | ||||
|     private ArrayList<UriMatcher> mChildren; | ||||
| } | ||||
|  | @ -0,0 +1,57 @@ | |||
| package com.getcapacitor; | ||||
| 
 | ||||
| import android.webkit.RenderProcessGoneDetail; | ||||
| import android.webkit.WebView; | ||||
| 
 | ||||
| /** | ||||
|  * Provides callbacks associated with the {@link BridgeWebViewClient} | ||||
|  */ | ||||
| public abstract class WebViewListener { | ||||
| 
 | ||||
|     /** | ||||
|      * Callback for page load event. | ||||
|      * | ||||
|      * @param webView The WebView that loaded | ||||
|      */ | ||||
|     public void onPageLoaded(WebView webView) { | ||||
|         // Override me to add behavior to the page loaded event | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback for onReceivedError event. | ||||
|      * | ||||
|      * @param webView The WebView that loaded | ||||
|      */ | ||||
|     public void onReceivedError(WebView webView) { | ||||
|         // Override me to add behavior to handle the onReceivedError event | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback for onReceivedHttpError event. | ||||
|      * | ||||
|      * @param webView The WebView that loaded | ||||
|      */ | ||||
|     public void onReceivedHttpError(WebView webView) { | ||||
|         // Override me to add behavior to handle the onReceivedHttpError event | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback for page start event. | ||||
|      * | ||||
|      * @param webView The WebView that loaded | ||||
|      */ | ||||
|     public void onPageStarted(WebView webView) { | ||||
|         // Override me to add behavior to the page started event | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Callback for render process gone event. Return true if the state is handled. | ||||
|      * | ||||
|      * @param webView The WebView that loaded | ||||
|      * @return returns false by default if the listener is not overridden and used | ||||
|      */ | ||||
|     public boolean onRenderProcessGone(WebView webView, RenderProcessGoneDetail detail) { | ||||
|         // Override me to add behavior to the web view render process gone event | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,749 @@ | |||
| /* | ||||
| Copyright 2015 Google Inc. All rights reserved. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
| You may obtain a copy of the License at | ||||
| 
 | ||||
|     http://www.apache.org/licenses/LICENSE-2.0 | ||||
| 
 | ||||
| Unless required by applicable law or agreed to in writing, software | ||||
| distributed under the License is distributed on an "AS IS" BASIS, | ||||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||
| See the License for the specific language governing permissions and | ||||
| limitations under the License. | ||||
|  */ | ||||
| package com.getcapacitor; | ||||
| 
 | ||||
| import static com.getcapacitor.plugin.util.HttpRequestHandler.isDomainExcludedFromSSL; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.net.Uri; | ||||
| import android.util.Base64; | ||||
| import android.webkit.CookieManager; | ||||
| import android.webkit.WebResourceRequest; | ||||
| import android.webkit.WebResourceResponse; | ||||
| import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection; | ||||
| import com.getcapacitor.plugin.util.HttpRequestHandler; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.URL; | ||||
| import java.net.URLConnection; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.ArrayList; | ||||
| import java.util.HashMap; | ||||
| import java.util.LinkedHashMap; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| /** | ||||
|  * Helper class meant to be used with the android.webkit.WebView class to enable hosting assets, | ||||
|  * resources and other data on 'virtual' https:// URL. | ||||
|  * Hosting assets and resources on https:// URLs is desirable as it is compatible with the | ||||
|  * Same-Origin policy. | ||||
|  * <p> | ||||
|  * This class is intended to be used from within the | ||||
|  * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, String)} and | ||||
|  * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, | ||||
|  * android.webkit.WebResourceRequest)} | ||||
|  * methods. | ||||
|  */ | ||||
| public class WebViewLocalServer { | ||||
| 
 | ||||
|     private static final String capacitorFileStart = Bridge.CAPACITOR_FILE_START; | ||||
|     private static final String capacitorContentStart = Bridge.CAPACITOR_CONTENT_START; | ||||
|     private String basePath; | ||||
| 
 | ||||
|     private final UriMatcher uriMatcher; | ||||
|     private final AndroidProtocolHandler protocolHandler; | ||||
|     private final ArrayList<String> authorities; | ||||
|     private boolean isAsset; | ||||
|     // Whether to route all requests to paths without extensions back to `index.html` | ||||
|     private final boolean html5mode; | ||||
|     private final JSInjector jsInjector; | ||||
|     private final Bridge bridge; | ||||
| 
 | ||||
|     /** | ||||
|      * A handler that produces responses for paths on the virtual asset server. | ||||
|      * <p> | ||||
|      * Methods of this handler will be invoked on a background thread and care must be taken to | ||||
|      * correctly synchronize access to any shared state. | ||||
|      * <p> | ||||
|      * On Android KitKat and above these methods may be called on more than one thread. This thread | ||||
|      * may be different than the thread on which the shouldInterceptRequest method was invoke. | ||||
|      * This means that on Android KitKat and above it is possible to block in this method without | ||||
|      * blocking other resources from loading. The number of threads used to parallelize loading | ||||
|      * is an internal implementation detail of the WebView and may change between updates which | ||||
|      * means that the amount of time spend blocking in this method should be kept to an absolute | ||||
|      * minimum. | ||||
|      */ | ||||
|     public abstract static class PathHandler { | ||||
| 
 | ||||
|         protected String mimeType; | ||||
|         private String encoding; | ||||
|         private String charset; | ||||
|         private int statusCode; | ||||
|         private String reasonPhrase; | ||||
|         private Map<String, String> responseHeaders; | ||||
| 
 | ||||
|         public PathHandler() { | ||||
|             this(null, null, 200, "OK", null); | ||||
|         } | ||||
| 
 | ||||
|         public PathHandler(String encoding, String charset, int statusCode, String reasonPhrase, Map<String, String> responseHeaders) { | ||||
|             this.encoding = encoding; | ||||
|             this.charset = charset; | ||||
|             this.statusCode = statusCode; | ||||
|             this.reasonPhrase = reasonPhrase; | ||||
|             Map<String, String> tempResponseHeaders; | ||||
|             if (responseHeaders == null) { | ||||
|                 tempResponseHeaders = new HashMap<>(); | ||||
|             } else { | ||||
|                 tempResponseHeaders = responseHeaders; | ||||
|             } | ||||
|             tempResponseHeaders.put("Cache-Control", "no-cache"); | ||||
|             this.responseHeaders = tempResponseHeaders; | ||||
|         } | ||||
| 
 | ||||
|         public InputStream handle(WebResourceRequest request) { | ||||
|             return handle(request.getUrl()); | ||||
|         } | ||||
| 
 | ||||
|         public abstract InputStream handle(Uri url); | ||||
| 
 | ||||
|         public String getEncoding() { | ||||
|             return encoding; | ||||
|         } | ||||
| 
 | ||||
|         public String getCharset() { | ||||
|             return charset; | ||||
|         } | ||||
| 
 | ||||
|         public int getStatusCode() { | ||||
|             return statusCode; | ||||
|         } | ||||
| 
 | ||||
|         public String getReasonPhrase() { | ||||
|             return reasonPhrase; | ||||
|         } | ||||
| 
 | ||||
|         public Map<String, String> getResponseHeaders() { | ||||
|             return responseHeaders; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     WebViewLocalServer(Context context, Bridge bridge, JSInjector jsInjector, ArrayList<String> authorities, boolean html5mode) { | ||||
|         uriMatcher = new UriMatcher(null); | ||||
|         this.html5mode = html5mode; | ||||
|         this.protocolHandler = new AndroidProtocolHandler(context.getApplicationContext()); | ||||
|         this.authorities = authorities; | ||||
|         this.bridge = bridge; | ||||
|         this.jsInjector = jsInjector; | ||||
|     } | ||||
| 
 | ||||
|     private static Uri parseAndVerifyUrl(String url) { | ||||
|         if (url == null) { | ||||
|             return null; | ||||
|         } | ||||
|         Uri uri = Uri.parse(url); | ||||
|         if (uri == null) { | ||||
|             Logger.error("Malformed URL: " + url); | ||||
|             return null; | ||||
|         } | ||||
|         String path = uri.getPath(); | ||||
|         if (path == null || path.isEmpty()) { | ||||
|             Logger.error("URL does not have a path: " + url); | ||||
|             return null; | ||||
|         } | ||||
|         return uri; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Attempt to retrieve the WebResourceResponse associated with the given <code>request</code>. | ||||
|      * This method should be invoked from within | ||||
|      * {@link android.webkit.WebViewClient#shouldInterceptRequest(android.webkit.WebView, | ||||
|      * android.webkit.WebResourceRequest)}. | ||||
|      * | ||||
|      * @param request the request to process. | ||||
|      * @return a response if the request URL had a matching handler, null if no handler was found. | ||||
|      */ | ||||
|     public WebResourceResponse shouldInterceptRequest(WebResourceRequest request) { | ||||
|         Uri loadingUrl = request.getUrl(); | ||||
| 
 | ||||
|         if (null != loadingUrl.getPath() && loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START)) { | ||||
|             Logger.debug("Handling CapacitorHttp request: " + loadingUrl); | ||||
|             try { | ||||
|                 return handleCapacitorHttpRequest(request); | ||||
|             } catch (Exception e) { | ||||
|                 Logger.error(e.getLocalizedMessage()); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         PathHandler handler; | ||||
|         synchronized (uriMatcher) { | ||||
|             handler = (PathHandler) uriMatcher.match(request.getUrl()); | ||||
|         } | ||||
|         if (handler == null) { | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl(loadingUrl)) { | ||||
|             Logger.debug("Handling local request: " + request.getUrl().toString()); | ||||
|             return handleLocalRequest(request, handler); | ||||
|         } else { | ||||
|             return handleProxyRequest(request, handler); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean isLocalFile(Uri uri) { | ||||
|         String path = uri.getPath(); | ||||
|         return path.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart); | ||||
|     } | ||||
| 
 | ||||
|     private boolean isErrorUrl(Uri uri) { | ||||
|         String url = uri.toString(); | ||||
|         return url.equals(bridge.getErrorUrl()); | ||||
|     } | ||||
| 
 | ||||
|     private boolean isMainUrl(Uri loadingUrl) { | ||||
|         return (bridge.getServerUrl() == null && loadingUrl.getHost().equalsIgnoreCase(bridge.getHost())); | ||||
|     } | ||||
| 
 | ||||
|     private boolean isAllowedUrl(Uri loadingUrl) { | ||||
|         return !(bridge.getServerUrl() == null && !bridge.getAppAllowNavigationMask().matches(loadingUrl.getHost())); | ||||
|     } | ||||
| 
 | ||||
|     private String getReasonPhraseFromResponseCode(int code) { | ||||
|         return switch (code) { | ||||
|             case 100 -> "Continue"; | ||||
|             case 101 -> "Switching Protocols"; | ||||
|             case 200 -> "OK"; | ||||
|             case 201 -> "Created"; | ||||
|             case 202 -> "Accepted"; | ||||
|             case 203 -> "Non-Authoritative Information"; | ||||
|             case 204 -> "No Content"; | ||||
|             case 205 -> "Reset Content"; | ||||
|             case 206 -> "Partial Content"; | ||||
|             case 300 -> "Multiple Choices"; | ||||
|             case 301 -> "Moved Permanently"; | ||||
|             case 302 -> "Found"; | ||||
|             case 303 -> "See Other"; | ||||
|             case 304 -> "Not Modified"; | ||||
|             case 400 -> "Bad Request"; | ||||
|             case 401 -> "Unauthorized"; | ||||
|             case 403 -> "Forbidden"; | ||||
|             case 404 -> "Not Found"; | ||||
|             case 405 -> "Method Not Allowed"; | ||||
|             case 406 -> "Not Acceptable"; | ||||
|             case 407 -> "Proxy Authentication Required"; | ||||
|             case 408 -> "Request Timeout"; | ||||
|             case 409 -> "Conflict"; | ||||
|             case 410 -> "Gone"; | ||||
|             case 500 -> "Internal Server Error"; | ||||
|             case 501 -> "Not Implemented"; | ||||
|             case 502 -> "Bad Gateway"; | ||||
|             case 503 -> "Service Unavailable"; | ||||
|             case 504 -> "Gateway Timeout"; | ||||
|             case 505 -> "HTTP Version Not Supported"; | ||||
|             default -> "Unknown"; | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     private WebResourceResponse handleCapacitorHttpRequest(WebResourceRequest request) throws IOException { | ||||
|         String urlString = request.getUrl().getQueryParameter(Bridge.CAPACITOR_HTTP_INTERCEPTOR_URL_PARAM); | ||||
|         URL url = new URL(urlString); | ||||
|         JSObject headers = new JSObject(); | ||||
| 
 | ||||
|         for (Map.Entry<String, String> header : request.getRequestHeaders().entrySet()) { | ||||
|             headers.put(header.getKey(), header.getValue()); | ||||
|         } | ||||
| 
 | ||||
|         HttpRequestHandler.HttpURLConnectionBuilder connectionBuilder = new HttpRequestHandler.HttpURLConnectionBuilder() | ||||
|             .setUrl(url) | ||||
|             .setMethod(request.getMethod()) | ||||
|             .setHeaders(headers) | ||||
|             .openConnection(); | ||||
| 
 | ||||
|         CapacitorHttpUrlConnection connection = connectionBuilder.build(); | ||||
| 
 | ||||
|         if (!isDomainExcludedFromSSL(bridge, url)) { | ||||
|             connection.setSSLSocketFactory(bridge); | ||||
|         } | ||||
| 
 | ||||
|         connection.connect(); | ||||
| 
 | ||||
|         String mimeType = null; | ||||
|         String encoding = null; | ||||
|         Map<String, String> responseHeaders = new LinkedHashMap<>(); | ||||
|         for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) { | ||||
|             StringBuilder builder = new StringBuilder(); | ||||
|             for (String value : entry.getValue()) { | ||||
|                 builder.append(value); | ||||
|                 builder.append(", "); | ||||
|             } | ||||
|             builder.setLength(builder.length() - 2); | ||||
| 
 | ||||
|             if ("Content-Type".equalsIgnoreCase(entry.getKey())) { | ||||
|                 String[] contentTypeParts = builder.toString().split(";"); | ||||
|                 mimeType = contentTypeParts[0].trim(); | ||||
|                 if (contentTypeParts.length > 1) { | ||||
|                     String[] encodingParts = contentTypeParts[1].split("="); | ||||
|                     if (encodingParts.length > 1) { | ||||
|                         encoding = encodingParts[1].trim(); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 responseHeaders.put(entry.getKey(), builder.toString()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         InputStream inputStream = connection.getErrorStream(); | ||||
|         if (inputStream == null) { | ||||
|             inputStream = connection.getInputStream(); | ||||
|         } | ||||
| 
 | ||||
|         if (null == mimeType) { | ||||
|             mimeType = getMimeType(request.getUrl().getPath(), inputStream); | ||||
|         } | ||||
| 
 | ||||
|         int responseCode = connection.getResponseCode(); | ||||
|         String reasonPhrase = getReasonPhraseFromResponseCode(responseCode); | ||||
| 
 | ||||
|         return new WebResourceResponse(mimeType, encoding, responseCode, reasonPhrase, responseHeaders, inputStream); | ||||
|     } | ||||
| 
 | ||||
|     private WebResourceResponse handleLocalRequest(WebResourceRequest request, PathHandler handler) { | ||||
|         String path = request.getUrl().getPath(); | ||||
| 
 | ||||
|         if (request.getRequestHeaders().get("Range") != null) { | ||||
|             InputStream responseStream = new LollipopLazyInputStream(handler, request); | ||||
|             String mimeType = getMimeType(path, responseStream); | ||||
|             Map<String, String> tempResponseHeaders = handler.getResponseHeaders(); | ||||
|             int statusCode = 206; | ||||
|             try { | ||||
|                 int totalRange = responseStream.available(); | ||||
|                 String rangeString = request.getRequestHeaders().get("Range"); | ||||
|                 String[] parts = rangeString.split("="); | ||||
|                 String[] streamParts = parts[1].split("-"); | ||||
|                 String fromRange = streamParts[0]; | ||||
|                 int range = totalRange - 1; | ||||
|                 if (streamParts.length > 1) { | ||||
|                     range = Integer.parseInt(streamParts[1]); | ||||
|                 } | ||||
|                 tempResponseHeaders.put("Accept-Ranges", "bytes"); | ||||
|                 tempResponseHeaders.put("Content-Range", "bytes " + fromRange + "-" + range + "/" + totalRange); | ||||
|             } catch (IOException e) { | ||||
|                 statusCode = 404; | ||||
|             } | ||||
|             return new WebResourceResponse( | ||||
|                 mimeType, | ||||
|                 handler.getEncoding(), | ||||
|                 statusCode, | ||||
|                 handler.getReasonPhrase(), | ||||
|                 tempResponseHeaders, | ||||
|                 responseStream | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (isLocalFile(request.getUrl()) || isErrorUrl(request.getUrl())) { | ||||
|             InputStream responseStream = new LollipopLazyInputStream(handler, request); | ||||
|             String mimeType = getMimeType(request.getUrl().getPath(), responseStream); | ||||
|             int statusCode = getStatusCode(responseStream, handler.getStatusCode()); | ||||
|             return new WebResourceResponse( | ||||
|                 mimeType, | ||||
|                 handler.getEncoding(), | ||||
|                 statusCode, | ||||
|                 handler.getReasonPhrase(), | ||||
|                 handler.getResponseHeaders(), | ||||
|                 responseStream | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (path.equals("/cordova.js")) { | ||||
|             return new WebResourceResponse( | ||||
|                 "application/javascript", | ||||
|                 handler.getEncoding(), | ||||
|                 handler.getStatusCode(), | ||||
|                 handler.getReasonPhrase(), | ||||
|                 handler.getResponseHeaders(), | ||||
|                 null | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if (path.equals("/") || (!request.getUrl().getLastPathSegment().contains(".") && html5mode)) { | ||||
|             InputStream responseStream; | ||||
|             try { | ||||
|                 String startPath = this.basePath + "/index.html"; | ||||
|                 if (bridge.getRouteProcessor() != null) { | ||||
|                     ProcessedRoute processedRoute = bridge.getRouteProcessor().process(this.basePath, "/index.html"); | ||||
|                     startPath = processedRoute.getPath(); | ||||
|                     isAsset = processedRoute.isAsset(); | ||||
|                 } | ||||
| 
 | ||||
|                 if (isAsset) { | ||||
|                     responseStream = protocolHandler.openAsset(startPath); | ||||
|                 } else { | ||||
|                     responseStream = protocolHandler.openFile(startPath); | ||||
|                 } | ||||
|             } catch (IOException e) { | ||||
|                 Logger.error("Unable to open index.html", e); | ||||
|                 return null; | ||||
|             } | ||||
| 
 | ||||
|             if (jsInjector != null) { | ||||
|                 responseStream = jsInjector.getInjectedStream(responseStream); | ||||
|             } | ||||
| 
 | ||||
|             int statusCode = getStatusCode(responseStream, handler.getStatusCode()); | ||||
|             return new WebResourceResponse( | ||||
|                 "text/html", | ||||
|                 handler.getEncoding(), | ||||
|                 statusCode, | ||||
|                 handler.getReasonPhrase(), | ||||
|                 handler.getResponseHeaders(), | ||||
|                 responseStream | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         if ("/favicon.ico".equalsIgnoreCase(path)) { | ||||
|             try { | ||||
|                 return new WebResourceResponse("image/png", null, null); | ||||
|             } catch (Exception e) { | ||||
|                 Logger.error("favicon handling failed", e); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         int periodIndex = path.lastIndexOf("."); | ||||
|         if (periodIndex >= 0) { | ||||
|             String ext = path.substring(path.lastIndexOf(".")); | ||||
| 
 | ||||
|             InputStream responseStream = new LollipopLazyInputStream(handler, request); | ||||
| 
 | ||||
|             // TODO: Conjure up a bit more subtlety than this | ||||
|             if (ext.equals(".html") && jsInjector != null) { | ||||
|                 responseStream = jsInjector.getInjectedStream(responseStream); | ||||
|             } | ||||
| 
 | ||||
|             String mimeType = getMimeType(path, responseStream); | ||||
|             int statusCode = getStatusCode(responseStream, handler.getStatusCode()); | ||||
|             return new WebResourceResponse( | ||||
|                 mimeType, | ||||
|                 handler.getEncoding(), | ||||
|                 statusCode, | ||||
|                 handler.getReasonPhrase(), | ||||
|                 handler.getResponseHeaders(), | ||||
|                 responseStream | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Prepends an {@code InputStream} with the JavaScript required by Capacitor. | ||||
|      * This method only changes the original {@code InputStream} if {@code WebView} does not | ||||
|      * support the {@code DOCUMENT_START_SCRIPT} feature. | ||||
|      * @param original the original {@code InputStream} | ||||
|      * @return the modified {@code InputStream} | ||||
|      */ | ||||
|     public InputStream getJavaScriptInjectedStream(InputStream original) { | ||||
|         if (jsInjector != null) { | ||||
|             return jsInjector.getInjectedStream(original); | ||||
|         } | ||||
|         return original; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Instead of reading files from the filesystem/assets, proxy through to the URL | ||||
|      * and let an external server handle it. | ||||
|      * @param request | ||||
|      * @param handler | ||||
|      * @return | ||||
|      */ | ||||
|     private WebResourceResponse handleProxyRequest(WebResourceRequest request, PathHandler handler) { | ||||
|         if (jsInjector != null) { | ||||
|             final String method = request.getMethod(); | ||||
|             if (method.equals("GET")) { | ||||
|                 try { | ||||
|                     String url = request.getUrl().toString(); | ||||
|                     Map<String, String> headers = request.getRequestHeaders(); | ||||
|                     boolean isHtmlText = false; | ||||
|                     for (Map.Entry<String, String> header : headers.entrySet()) { | ||||
|                         if (header.getKey().equalsIgnoreCase("Accept") && header.getValue().toLowerCase().contains("text/html")) { | ||||
|                             isHtmlText = true; | ||||
|                             break; | ||||
|                         } | ||||
|                     } | ||||
|                     if (isHtmlText) { | ||||
|                         HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); | ||||
|                         for (Map.Entry<String, String> header : headers.entrySet()) { | ||||
|                             conn.setRequestProperty(header.getKey(), header.getValue()); | ||||
|                         } | ||||
|                         String getCookie = CookieManager.getInstance().getCookie(url); | ||||
|                         if (getCookie != null) { | ||||
|                             conn.setRequestProperty("Cookie", getCookie); | ||||
|                         } | ||||
|                         conn.setRequestMethod(method); | ||||
|                         conn.setReadTimeout(30 * 1000); | ||||
|                         conn.setConnectTimeout(30 * 1000); | ||||
|                         if (request.getUrl().getUserInfo() != null) { | ||||
|                             byte[] userInfoBytes = request.getUrl().getUserInfo().getBytes(StandardCharsets.UTF_8); | ||||
|                             String base64 = Base64.encodeToString(userInfoBytes, Base64.NO_WRAP); | ||||
|                             conn.setRequestProperty("Authorization", "Basic " + base64); | ||||
|                         } | ||||
| 
 | ||||
|                         List<String> cookies = conn.getHeaderFields().get("Set-Cookie"); | ||||
|                         if (cookies != null) { | ||||
|                             for (String cookie : cookies) { | ||||
|                                 CookieManager.getInstance().setCookie(url, cookie); | ||||
|                             } | ||||
|                         } | ||||
|                         InputStream responseStream = conn.getInputStream(); | ||||
|                         responseStream = jsInjector.getInjectedStream(responseStream); | ||||
| 
 | ||||
|                         return new WebResourceResponse( | ||||
|                             "text/html", | ||||
|                             handler.getEncoding(), | ||||
|                             handler.getStatusCode(), | ||||
|                             handler.getReasonPhrase(), | ||||
|                             handler.getResponseHeaders(), | ||||
|                             responseStream | ||||
|                         ); | ||||
|                     } | ||||
|                 } catch (Exception ex) { | ||||
|                     bridge.handleAppUrlLoadError(ex); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     private String getMimeType(String path, InputStream stream) { | ||||
|         String mimeType = null; | ||||
|         try { | ||||
|             mimeType = URLConnection.guessContentTypeFromName(path); // Does not recognize *.js | ||||
|             if (mimeType != null && path.endsWith(".js") && mimeType.equals("image/x-icon")) { | ||||
|                 Logger.debug("We shouldn't be here"); | ||||
|             } | ||||
|             if (mimeType == null) { | ||||
|                 if (path.endsWith(".js") || path.endsWith(".mjs")) { | ||||
|                     // Make sure JS files get the proper mimetype to support ES modules | ||||
|                     mimeType = "application/javascript"; | ||||
|                 } else if (path.endsWith(".wasm")) { | ||||
|                     mimeType = "application/wasm"; | ||||
|                 } else { | ||||
|                     mimeType = URLConnection.guessContentTypeFromStream(stream); | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception ex) { | ||||
|             Logger.error("Unable to get mime type" + path, ex); | ||||
|         } | ||||
|         return mimeType; | ||||
|     } | ||||
| 
 | ||||
|     private int getStatusCode(InputStream stream, int defaultCode) { | ||||
|         int finalStatusCode = defaultCode; | ||||
|         try { | ||||
|             if (stream.available() == -1) { | ||||
|                 finalStatusCode = 404; | ||||
|             } | ||||
|         } catch (IOException e) { | ||||
|             finalStatusCode = 500; | ||||
|         } | ||||
|         return finalStatusCode; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Registers a handler for the given <code>uri</code>. The <code>handler</code> will be invoked | ||||
|      * every time the <code>shouldInterceptRequest</code> method of the instance is called with | ||||
|      * a matching <code>uri</code>. | ||||
|      * | ||||
|      * @param uri     the uri to use the handler for. The scheme and authority (domain) will be matched | ||||
|      *                exactly. The path may contain a '*' element which will match a single element of | ||||
|      *                a path (so a handler registered for /a/* will be invoked for /a/b and /a/c.html | ||||
|      *                but not for /a/b/b) or the '**' element which will match any number of path | ||||
|      *                elements. | ||||
|      * @param handler the handler to use for the uri. | ||||
|      */ | ||||
|     void register(Uri uri, PathHandler handler) { | ||||
|         synchronized (uriMatcher) { | ||||
|             uriMatcher.addURI(uri.getScheme(), uri.getAuthority(), uri.getPath(), handler); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hosts the application's assets on an https:// URL. Assets from the local path | ||||
|      * <code>assetPath/...</code> will be available under | ||||
|      * <code>https://{uuid}.androidplatform.net/assets/...</code>. | ||||
|      * | ||||
|      * @param assetPath the local path in the application's asset folder which will be made | ||||
|      *                  available by the server (for example "/www"). | ||||
|      * @return prefixes under which the assets are hosted. | ||||
|      */ | ||||
|     public void hostAssets(String assetPath) { | ||||
|         this.isAsset = true; | ||||
|         this.basePath = assetPath; | ||||
|         createHostingDetails(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Hosts the application's files on an https:// URL. Files from the basePath | ||||
|      * <code>basePath/...</code> will be available under | ||||
|      * <code>https://{uuid}.androidplatform.net/...</code>. | ||||
|      * | ||||
|      * @param basePath the local path in the application's data folder which will be made | ||||
|      *                  available by the server (for example "/www"). | ||||
|      * @return prefixes under which the assets are hosted. | ||||
|      */ | ||||
|     public void hostFiles(final String basePath) { | ||||
|         this.isAsset = false; | ||||
|         this.basePath = basePath; | ||||
|         createHostingDetails(); | ||||
|     } | ||||
| 
 | ||||
|     private void createHostingDetails() { | ||||
|         final String assetPath = this.basePath; | ||||
| 
 | ||||
|         if (assetPath.indexOf('*') != -1) { | ||||
|             throw new IllegalArgumentException("assetPath cannot contain the '*' character."); | ||||
|         } | ||||
| 
 | ||||
|         PathHandler handler = new PathHandler() { | ||||
|             @Override | ||||
|             public InputStream handle(Uri url) { | ||||
|                 InputStream stream = null; | ||||
|                 String path = url.getPath(); | ||||
| 
 | ||||
|                 // Pass path to routeProcessor if present | ||||
|                 RouteProcessor routeProcessor = bridge.getRouteProcessor(); | ||||
|                 boolean ignoreAssetPath = false; | ||||
|                 if (routeProcessor != null) { | ||||
|                     ProcessedRoute processedRoute = bridge.getRouteProcessor().process("", path); | ||||
|                     path = processedRoute.getPath(); | ||||
|                     isAsset = processedRoute.isAsset(); | ||||
|                     ignoreAssetPath = processedRoute.isIgnoreAssetPath(); | ||||
|                 } | ||||
| 
 | ||||
|                 try { | ||||
|                     if (path.startsWith(capacitorContentStart)) { | ||||
|                         stream = protocolHandler.openContentUrl(url); | ||||
|                     } else if (path.startsWith(capacitorFileStart)) { | ||||
|                         stream = protocolHandler.openFile(path); | ||||
|                     } else if (!isAsset) { | ||||
|                         if (routeProcessor == null) { | ||||
|                             path = basePath + url.getPath(); | ||||
|                         } | ||||
| 
 | ||||
|                         stream = protocolHandler.openFile(path); | ||||
|                     } else if (ignoreAssetPath) { | ||||
|                         stream = protocolHandler.openAsset(path); | ||||
|                     } else { | ||||
|                         stream = protocolHandler.openAsset(assetPath + path); | ||||
|                     } | ||||
|                 } catch (IOException e) { | ||||
|                     Logger.error("Unable to open asset URL: " + url); | ||||
|                     return null; | ||||
|                 } | ||||
| 
 | ||||
|                 return stream; | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         for (String authority : authorities) { | ||||
|             registerUriForScheme(Bridge.CAPACITOR_HTTP_SCHEME, handler, authority); | ||||
|             registerUriForScheme(Bridge.CAPACITOR_HTTPS_SCHEME, handler, authority); | ||||
| 
 | ||||
|             String customScheme = this.bridge.getScheme(); | ||||
|             if (!customScheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !customScheme.equals(Bridge.CAPACITOR_HTTPS_SCHEME)) { | ||||
|                 registerUriForScheme(customScheme, handler, authority); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void registerUriForScheme(String scheme, PathHandler handler, String authority) { | ||||
|         Uri.Builder uriBuilder = new Uri.Builder(); | ||||
|         uriBuilder.scheme(scheme); | ||||
|         uriBuilder.authority(authority); | ||||
|         uriBuilder.path(""); | ||||
|         Uri uriPrefix = uriBuilder.build(); | ||||
| 
 | ||||
|         register(Uri.withAppendedPath(uriPrefix, "/"), handler); | ||||
|         register(Uri.withAppendedPath(uriPrefix, "**"), handler); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The KitKat WebView reads the InputStream on a separate threadpool. We can use that to | ||||
|      * parallelize loading. | ||||
|      */ | ||||
|     private abstract static class LazyInputStream extends InputStream { | ||||
| 
 | ||||
|         protected final PathHandler handler; | ||||
|         private InputStream is = null; | ||||
| 
 | ||||
|         public LazyInputStream(PathHandler handler) { | ||||
|             this.handler = handler; | ||||
|         } | ||||
| 
 | ||||
|         private InputStream getInputStream() { | ||||
|             if (is == null) { | ||||
|                 is = handle(); | ||||
|             } | ||||
|             return is; | ||||
|         } | ||||
| 
 | ||||
|         protected abstract InputStream handle(); | ||||
| 
 | ||||
|         @Override | ||||
|         public int available() throws IOException { | ||||
|             InputStream is = getInputStream(); | ||||
|             return (is != null) ? is.available() : -1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int read() throws IOException { | ||||
|             InputStream is = getInputStream(); | ||||
|             return (is != null) ? is.read() : -1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int read(byte[] b) throws IOException { | ||||
|             InputStream is = getInputStream(); | ||||
|             return (is != null) ? is.read(b) : -1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int read(byte[] b, int off, int len) throws IOException { | ||||
|             InputStream is = getInputStream(); | ||||
|             return (is != null) ? is.read(b, off, len) : -1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public long skip(long n) throws IOException { | ||||
|             InputStream is = getInputStream(); | ||||
|             return (is != null) ? is.skip(n) : 0; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // For L and above. | ||||
|     private static class LollipopLazyInputStream extends LazyInputStream { | ||||
| 
 | ||||
|         private WebResourceRequest request; | ||||
|         private InputStream is; | ||||
| 
 | ||||
|         public LollipopLazyInputStream(PathHandler handler, WebResourceRequest request) { | ||||
|             super(handler); | ||||
|             this.request = request; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         protected InputStream handle() { | ||||
|             return handler.handle(request); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public String getBasePath() { | ||||
|         return this.basePath; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,11 @@ | |||
| package com.getcapacitor.annotation; | ||||
| 
 | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
| 
 | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Target(ElementType.METHOD) | ||||
| public @interface ActivityCallback { | ||||
| } | ||||
|  | @ -0,0 +1,35 @@ | |||
| package com.getcapacitor.annotation; | ||||
| 
 | ||||
| import android.content.Intent; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| /** | ||||
|  * Base annotation for all Plugins | ||||
|  */ | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface CapacitorPlugin { | ||||
|     /** | ||||
|      * A custom name for the plugin, otherwise uses the | ||||
|      * simple class name. | ||||
|      */ | ||||
|     String name() default ""; | ||||
| 
 | ||||
|     /** | ||||
|      * Request codes this plugin uses and responds to, in order to tie | ||||
|      * Android events back the plugin to handle. | ||||
|      * | ||||
|      * NOTE: This is a legacy option provided to support third party libraries | ||||
|      * not currently implementing the new AndroidX Activity Results API. Plugins | ||||
|      * without this limitation should use a registered callback with | ||||
|      * {@link com.getcapacitor.Plugin#startActivityForResult(PluginCall, Intent, String)} | ||||
|      */ | ||||
|     int[] requestCodes() default {}; | ||||
| 
 | ||||
|     /** | ||||
|      * Permissions this plugin needs, in order to make permission requests | ||||
|      * easy if the plugin only needs basic permission prompting | ||||
|      */ | ||||
|     Permission[] permissions() default {}; | ||||
| } | ||||
|  | @ -0,0 +1,22 @@ | |||
| package com.getcapacitor.annotation; | ||||
| 
 | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| 
 | ||||
| /** | ||||
|  * Permission annotation for use with @CapacitorPlugin | ||||
|  */ | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| public @interface Permission { | ||||
|     /** | ||||
|      * An array of Android permission strings. | ||||
|      * Eg: {Manifest.permission.ACCESS_COARSE_LOCATION} | ||||
|      *     or {"android.permission.ACCESS_COARSE_LOCATION"} | ||||
|      */ | ||||
|     String[] strings() default {}; | ||||
| 
 | ||||
|     /** | ||||
|      * An optional name to use instead of the Android permission string. | ||||
|      */ | ||||
|     String alias() default ""; | ||||
| } | ||||
|  | @ -0,0 +1,11 @@ | |||
| package com.getcapacitor.annotation; | ||||
| 
 | ||||
| import java.lang.annotation.ElementType; | ||||
| import java.lang.annotation.Retention; | ||||
| import java.lang.annotation.RetentionPolicy; | ||||
| import java.lang.annotation.Target; | ||||
| 
 | ||||
| @Retention(RetentionPolicy.RUNTIME) | ||||
| @Target(ElementType.METHOD) | ||||
| public @interface PermissionCallback { | ||||
| } | ||||
|  | @ -0,0 +1,42 @@ | |||
| package com.getcapacitor.cordova; | ||||
| 
 | ||||
| import android.webkit.CookieManager; | ||||
| import android.webkit.WebView; | ||||
| import org.apache.cordova.ICordovaCookieManager; | ||||
| 
 | ||||
| class CapacitorCordovaCookieManager implements ICordovaCookieManager { | ||||
| 
 | ||||
|     protected final WebView webView; | ||||
|     private final CookieManager cookieManager; | ||||
| 
 | ||||
|     public CapacitorCordovaCookieManager(WebView webview) { | ||||
|         webView = webview; | ||||
|         cookieManager = CookieManager.getInstance(); | ||||
|         cookieManager.setAcceptThirdPartyCookies(webView, true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setCookiesEnabled(boolean accept) { | ||||
|         cookieManager.setAcceptCookie(accept); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setCookie(final String url, final String value) { | ||||
|         cookieManager.setCookie(url, value); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getCookie(final String url) { | ||||
|         return cookieManager.getCookie(url); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void clearCookies() { | ||||
|         cookieManager.removeAllCookies(null); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void flush() { | ||||
|         cookieManager.flush(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,39 @@ | |||
| package com.getcapacitor.cordova; | ||||
| 
 | ||||
| import android.util.Pair; | ||||
| import androidx.appcompat.app.AppCompatActivity; | ||||
| import java.util.concurrent.Executors; | ||||
| import org.apache.cordova.CordovaInterfaceImpl; | ||||
| import org.apache.cordova.CordovaPlugin; | ||||
| import org.json.JSONException; | ||||
| 
 | ||||
| public class MockCordovaInterfaceImpl extends CordovaInterfaceImpl { | ||||
| 
 | ||||
|     public MockCordovaInterfaceImpl(AppCompatActivity activity) { | ||||
|         super(activity, Executors.newCachedThreadPool()); | ||||
|     } | ||||
| 
 | ||||
|     public CordovaPlugin getActivityResultCallback() { | ||||
|         return this.activityResultCallback; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Checks Cordova permission callbacks to handle permissions defined by a Cordova plugin. | ||||
|      * Returns true if Cordova is handling the permission request with a registered code. | ||||
|      * | ||||
|      * @param requestCode | ||||
|      * @param permissions | ||||
|      * @param grantResults | ||||
|      * @return true if Cordova handled the permission request, false if not | ||||
|      */ | ||||
|     @SuppressWarnings("deprecation") | ||||
|     public boolean handlePermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException { | ||||
|         Pair<CordovaPlugin, Integer> callback = permissionResultCallbacks.getAndRemoveCallback(requestCode); | ||||
|         if (callback != null) { | ||||
|             callback.first.onRequestPermissionResult(callback.second, permissions, grantResults); | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,284 @@ | |||
| package com.getcapacitor.cordova; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.Intent; | ||||
| import android.os.Handler; | ||||
| import android.view.View; | ||||
| import android.webkit.ValueCallback; | ||||
| import android.webkit.WebChromeClient; | ||||
| import android.webkit.WebView; | ||||
| import java.util.List; | ||||
| import java.util.Map; | ||||
| import org.apache.cordova.CordovaInterface; | ||||
| import org.apache.cordova.CordovaPreferences; | ||||
| import org.apache.cordova.CordovaResourceApi; | ||||
| import org.apache.cordova.CordovaWebView; | ||||
| import org.apache.cordova.CordovaWebViewEngine; | ||||
| import org.apache.cordova.ICordovaCookieManager; | ||||
| import org.apache.cordova.NativeToJsMessageQueue; | ||||
| import org.apache.cordova.PluginEntry; | ||||
| import org.apache.cordova.PluginManager; | ||||
| import org.apache.cordova.PluginResult; | ||||
| 
 | ||||
| public class MockCordovaWebViewImpl implements CordovaWebView { | ||||
| 
 | ||||
|     private Context context; | ||||
|     private PluginManager pluginManager; | ||||
|     private CordovaPreferences preferences; | ||||
|     private CordovaResourceApi resourceApi; | ||||
|     private NativeToJsMessageQueue nativeToJsMessageQueue; | ||||
|     private CordovaInterface cordova; | ||||
|     private CapacitorCordovaCookieManager cookieManager; | ||||
|     private WebView webView; | ||||
|     private boolean hasPausedEver; | ||||
| 
 | ||||
|     public MockCordovaWebViewImpl(Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences) { | ||||
|         this.cordova = cordova; | ||||
|         this.preferences = preferences; | ||||
|         this.pluginManager = new PluginManager(this, this.cordova, pluginEntries); | ||||
|         this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager); | ||||
|         this.pluginManager.init(); | ||||
|     } | ||||
| 
 | ||||
|     public void init(CordovaInterface cordova, List<PluginEntry> pluginEntries, CordovaPreferences preferences, WebView webView) { | ||||
|         this.cordova = cordova; | ||||
|         this.webView = webView; | ||||
|         this.preferences = preferences; | ||||
|         this.pluginManager = new PluginManager(this, this.cordova, pluginEntries); | ||||
|         this.resourceApi = new CordovaResourceApi(this.context, this.pluginManager); | ||||
|         nativeToJsMessageQueue = new NativeToJsMessageQueue(); | ||||
|         nativeToJsMessageQueue.addBridgeMode(new CapacitorEvalBridgeMode(webView, this.cordova)); | ||||
|         nativeToJsMessageQueue.setBridgeMode(0); | ||||
|         this.cookieManager = new CapacitorCordovaCookieManager(webView); | ||||
|         this.pluginManager.init(); | ||||
|     } | ||||
| 
 | ||||
|     public static class CapacitorEvalBridgeMode extends NativeToJsMessageQueue.BridgeMode { | ||||
| 
 | ||||
|         private final WebView webView; | ||||
|         private final CordovaInterface cordova; | ||||
| 
 | ||||
|         public CapacitorEvalBridgeMode(WebView webView, CordovaInterface cordova) { | ||||
|             this.webView = webView; | ||||
|             this.cordova = cordova; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void onNativeToJsMessageAvailable(final NativeToJsMessageQueue queue) { | ||||
|             cordova | ||||
|                 .getActivity() | ||||
|                 .runOnUiThread( | ||||
|                     () -> { | ||||
|                         String js = queue.popAndEncodeAsJs(); | ||||
|                         if (js != null) { | ||||
|                             webView.evaluateJavascript(js, null); | ||||
|                         } | ||||
|                     } | ||||
|                 ); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isInitialized() { | ||||
|         return cordova != null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public View getView() { | ||||
|         return this.webView; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void loadUrlIntoView(String url, boolean recreatePlugins) { | ||||
|         if (url.equals("about:blank") || url.startsWith("javascript:")) { | ||||
|             webView.loadUrl(url); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void stopLoading() {} | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean canGoBack() { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void clearCache() {} | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public void clearCache(boolean b) {} | ||||
| 
 | ||||
|     @Override | ||||
|     public void clearHistory() {} | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean backHistory() { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handlePause(boolean keepRunning) { | ||||
|         if (!isInitialized()) { | ||||
|             return; | ||||
|         } | ||||
|         hasPausedEver = true; | ||||
|         pluginManager.onPause(keepRunning); | ||||
|         triggerDocumentEvent("pause"); | ||||
|         // If app doesn't want to run in background | ||||
|         if (!keepRunning) { | ||||
|             // Pause JavaScript timers. This affects all webviews within the app! | ||||
|             this.setPaused(true); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onNewIntent(Intent intent) { | ||||
|         if (this.pluginManager != null) { | ||||
|             this.pluginManager.onNewIntent(intent); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleResume(boolean keepRunning) { | ||||
|         if (!isInitialized()) { | ||||
|             return; | ||||
|         } | ||||
|         this.setPaused(false); | ||||
|         this.pluginManager.onResume(keepRunning); | ||||
|         if (hasPausedEver) { | ||||
|             triggerDocumentEvent("resume"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleStart() { | ||||
|         if (!isInitialized()) { | ||||
|             return; | ||||
|         } | ||||
|         pluginManager.onStart(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleStop() { | ||||
|         if (!isInitialized()) { | ||||
|             return; | ||||
|         } | ||||
|         pluginManager.onStop(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void handleDestroy() { | ||||
|         if (!isInitialized()) { | ||||
|             return; | ||||
|         } | ||||
|         this.pluginManager.onDestroy(); | ||||
|     } | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public void sendJavascript(String statememt) { | ||||
|         nativeToJsMessageQueue.addJavaScript(statememt); | ||||
|     } | ||||
| 
 | ||||
|     public void eval(final String js, final ValueCallback<String> callback) { | ||||
|         Handler mainHandler = new Handler(context.getMainLooper()); | ||||
|         mainHandler.post(() -> webView.evaluateJavascript(js, callback)); | ||||
|     } | ||||
| 
 | ||||
|     public void triggerDocumentEvent(final String eventName) { | ||||
|         eval("window.Capacitor.triggerEvent('" + eventName + "', 'document');", s -> {}); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void showWebPage(String url, boolean openExternal, boolean clearHistory, Map<String, Object> params) {} | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public boolean isCustomViewShowing() { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public void showCustomView(View view, WebChromeClient.CustomViewCallback callback) {} | ||||
| 
 | ||||
|     @Deprecated | ||||
|     @Override | ||||
|     public void hideCustomView() {} | ||||
| 
 | ||||
|     @Override | ||||
|     public CordovaResourceApi getResourceApi() { | ||||
|         return this.resourceApi; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setButtonPlumbedToJs(int keyCode, boolean override) {} | ||||
| 
 | ||||
|     @Override | ||||
|     public boolean isButtonPlumbedToJs(int keyCode) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void sendPluginResult(PluginResult cr, String callbackId) { | ||||
|         nativeToJsMessageQueue.addPluginResult(cr, callbackId); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public PluginManager getPluginManager() { | ||||
|         return this.pluginManager; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public CordovaWebViewEngine getEngine() { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public CordovaPreferences getPreferences() { | ||||
|         return this.preferences; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public ICordovaCookieManager getCookieManager() { | ||||
|         return cookieManager; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String getUrl() { | ||||
|         return webView.getUrl(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Context getContext() { | ||||
|         return this.webView.getContext(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void loadUrl(String url) { | ||||
|         loadUrlIntoView(url, true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Object postMessage(String id, Object data) { | ||||
|         return pluginManager.postMessage(id, data); | ||||
|     } | ||||
| 
 | ||||
|     public void setPaused(boolean value) { | ||||
|         if (value) { | ||||
|             webView.onPause(); | ||||
|             webView.pauseTimers(); | ||||
|         } else { | ||||
|             webView.onResume(); | ||||
|             webView.resumeTimers(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,239 @@ | |||
| package com.getcapacitor.plugin; | ||||
| 
 | ||||
| import com.getcapacitor.Bridge; | ||||
| import com.getcapacitor.Logger; | ||||
| import java.net.CookieManager; | ||||
| import java.net.CookiePolicy; | ||||
| import java.net.CookieStore; | ||||
| import java.net.HttpCookie; | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import java.util.Objects; | ||||
| 
 | ||||
| public class CapacitorCookieManager extends CookieManager { | ||||
| 
 | ||||
|     private final android.webkit.CookieManager webkitCookieManager; | ||||
| 
 | ||||
|     private final String localUrl; | ||||
| 
 | ||||
|     private final String serverUrl; | ||||
| 
 | ||||
|     private final String TAG = "CapacitorCookies"; | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new cookie manager with the default cookie store and policy | ||||
|      */ | ||||
|     public CapacitorCookieManager(Bridge bridge) { | ||||
|         this(null, null, bridge); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new cookie manager with specified cookie store and cookie policy. | ||||
|      * @param store a {@code CookieStore} to be used by CookieManager. if {@code null}, cookie | ||||
|      *              manager will use a default one, which is an in-memory CookieStore implementation. | ||||
|      * @param policy a {@code CookiePolicy} instance to be used by cookie manager as policy | ||||
|      *               callback. if {@code null}, ACCEPT_ORIGINAL_SERVER will be used. | ||||
|      */ | ||||
|     public CapacitorCookieManager(CookieStore store, CookiePolicy policy, Bridge bridge) { | ||||
|         super(store, policy); | ||||
|         webkitCookieManager = android.webkit.CookieManager.getInstance(); | ||||
|         this.localUrl = bridge.getLocalUrl(); | ||||
|         this.serverUrl = bridge.getServerUrl(); | ||||
|     } | ||||
| 
 | ||||
|     public void removeSessionCookies() { | ||||
|         this.webkitCookieManager.removeSessionCookies(null); | ||||
|     } | ||||
| 
 | ||||
|     public String getSanitizedDomain(String url) throws URISyntaxException { | ||||
|         if (this.serverUrl != null && !this.serverUrl.isEmpty() && (url == null || url.isEmpty() || this.serverUrl.contains(url))) { | ||||
|             url = this.serverUrl; | ||||
|         } else if (this.localUrl != null && !this.localUrl.isEmpty() && (url == null || url.isEmpty() || this.localUrl.contains(url))) { | ||||
|             url = this.localUrl; | ||||
|         } else try { | ||||
|             URI uri = new URI(url); | ||||
|             String scheme = uri.getScheme(); | ||||
|             if (scheme == null || scheme.isEmpty()) { | ||||
|                 url = "https://" + url; | ||||
|             } | ||||
|         } catch (URISyntaxException e) { | ||||
|             Logger.error(TAG, "Failed to get scheme from URL.", e); | ||||
|         } | ||||
| 
 | ||||
|         try { | ||||
|             new URI(url); | ||||
|         } catch (Exception error) { | ||||
|             Logger.error(TAG, "Failed to get sanitized URL.", error); | ||||
|             throw error; | ||||
|         } | ||||
|         return url; | ||||
|     } | ||||
| 
 | ||||
|     private String getDomainFromCookieString(String cookie) throws URISyntaxException { | ||||
|         String[] domain = cookie.toLowerCase(Locale.ROOT).split("domain="); | ||||
|         return getSanitizedDomain(domain.length <= 1 ? null : domain[1].split(";")[0].trim()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the cookies for the given URL. | ||||
|      * @param url the URL for which the cookies are requested | ||||
|      * @return value the cookies as a string, using the format of the 'Cookie' HTTP request header | ||||
|      */ | ||||
|     public String getCookieString(String url) { | ||||
|         try { | ||||
|             url = getSanitizedDomain(url); | ||||
|             Logger.info(TAG, "Getting cookies at: '" + url + "'"); | ||||
|             return webkitCookieManager.getCookie(url); | ||||
|         } catch (Exception error) { | ||||
|             Logger.error(TAG, "Failed to get cookies at the given URL.", error); | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets a cookie value for the given URL and key. | ||||
|      * @param url the URL for which the cookies are requested | ||||
|      * @param key the key of the cookie to search for | ||||
|      * @return the {@code HttpCookie} value of the cookie at the key, | ||||
|      *         otherwise it will return a new empty {@code HttpCookie} | ||||
|      */ | ||||
|     public HttpCookie getCookie(String url, String key) { | ||||
|         HttpCookie[] cookies = getCookies(url); | ||||
|         for (HttpCookie cookie : cookies) { | ||||
|             if (cookie.getName().equals(key)) { | ||||
|                 return cookie; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets an array of {@code HttpCookie} given a URL. | ||||
|      * @param url the URL for which the cookies are requested | ||||
|      * @return an {@code HttpCookie} array of non-expired cookies | ||||
|      */ | ||||
|     public HttpCookie[] getCookies(String url) { | ||||
|         try { | ||||
|             ArrayList<HttpCookie> cookieList = new ArrayList<>(); | ||||
|             String cookieString = getCookieString(url); | ||||
|             if (cookieString != null) { | ||||
|                 String[] singleCookie = cookieString.split(";"); | ||||
|                 for (String c : singleCookie) { | ||||
|                     HttpCookie parsed = HttpCookie.parse(c).get(0); | ||||
|                     parsed.setValue(parsed.getValue()); | ||||
|                     cookieList.add(parsed); | ||||
|                 } | ||||
|             } | ||||
|             HttpCookie[] cookies = new HttpCookie[cookieList.size()]; | ||||
|             return cookieList.toArray(cookies); | ||||
|         } catch (Exception ex) { | ||||
|             return new HttpCookie[0]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will | ||||
|      *  be replaced with the new cookie. The cookie being set will be ignored if it is expired. | ||||
|      * @param url the URL for which the cookie is to be set | ||||
|      * @param value the cookie as a string, using the format of the 'Set-Cookie' HTTP response header | ||||
|      */ | ||||
|     public void setCookie(String url, String value) { | ||||
|         try { | ||||
|             url = getSanitizedDomain(url); | ||||
|             Logger.info(TAG, "Setting cookie '" + value + "' at: '" + url + "'"); | ||||
|             webkitCookieManager.setCookie(url, value); | ||||
|             flush(); | ||||
|         } catch (Exception error) { | ||||
|             Logger.error(TAG, "Failed to set cookie.", error); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets a cookie for the given URL. Any existing cookie with the same host, path and name will | ||||
|      *  be replaced with the new cookie. The cookie being set will be ignored if it is expired. | ||||
|      * @param url the URL for which the cookie is to be set | ||||
|      * @param key the {@code HttpCookie} name to use for lookup | ||||
|      * @param value the value of the {@code HttpCookie} given a key | ||||
|      */ | ||||
|     public void setCookie(String url, String key, String value) { | ||||
|         String cookieValue = key + "=" + value; | ||||
|         setCookie(url, cookieValue); | ||||
|     } | ||||
| 
 | ||||
|     public void setCookie(String url, String key, String value, String expires, String path) { | ||||
|         String cookieValue = key + "=" + value + "; expires=" + expires + "; path=" + path; | ||||
|         setCookie(url, cookieValue); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Removes all cookies. This method is asynchronous. | ||||
|      */ | ||||
|     public void removeAllCookies() { | ||||
|         webkitCookieManager.removeAllCookies(null); | ||||
|         flush(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Ensures all cookies currently accessible through the getCookie API are written to persistent | ||||
|      *  storage. This call will block the caller until it is done and may perform I/O. | ||||
|      */ | ||||
|     public void flush() { | ||||
|         webkitCookieManager.flush(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void put(URI uri, Map<String, List<String>> responseHeaders) { | ||||
|         // make sure our args are valid | ||||
|         if ((uri == null) || (responseHeaders == null)) return; | ||||
| 
 | ||||
|         // go over the headers | ||||
|         for (String headerKey : responseHeaders.keySet()) { | ||||
|             // ignore headers which aren't cookie related | ||||
|             if ((headerKey == null) || !(headerKey.equalsIgnoreCase("Set-Cookie2") || headerKey.equalsIgnoreCase("Set-Cookie"))) continue; | ||||
| 
 | ||||
|             // process each of the headers | ||||
|             for (String headerValue : Objects.requireNonNull(responseHeaders.get(headerKey))) { | ||||
|                 try { | ||||
|                     // Set at the requested server url | ||||
|                     setCookie(uri.toString(), headerValue); | ||||
| 
 | ||||
|                     // Set at the defined domain in the response or at default capacitor hosted url | ||||
|                     setCookie(getDomainFromCookieString(headerValue), headerValue); | ||||
|                 } catch (Exception ignored) {} | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public Map<String, List<String>> get(URI uri, Map<String, List<String>> requestHeaders) { | ||||
|         // make sure our args are valid | ||||
|         if ((uri == null) || (requestHeaders == null)) throw new IllegalArgumentException("Argument is null"); | ||||
| 
 | ||||
|         // save our url once | ||||
|         String url = uri.toString(); | ||||
| 
 | ||||
|         // prepare our response | ||||
|         Map<String, List<String>> res = new HashMap<>(); | ||||
| 
 | ||||
|         // get the cookie | ||||
|         String cookie = getCookieString(url); | ||||
| 
 | ||||
|         // return it | ||||
|         if (cookie != null) res.put("Cookie", Collections.singletonList(cookie)); | ||||
|         return res; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public CookieStore getCookieStore() { | ||||
|         // we don't want anyone to work with this cookie store directly | ||||
|         throw new UnsupportedOperationException(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,122 @@ | |||
| package com.getcapacitor.plugin; | ||||
| 
 | ||||
| import android.webkit.JavascriptInterface; | ||||
| import com.getcapacitor.JSObject; | ||||
| import com.getcapacitor.Plugin; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import com.getcapacitor.PluginConfig; | ||||
| import com.getcapacitor.PluginMethod; | ||||
| import com.getcapacitor.annotation.CapacitorPlugin; | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.net.CookieHandler; | ||||
| import java.net.HttpCookie; | ||||
| import java.net.URLDecoder; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| 
 | ||||
| @CapacitorPlugin | ||||
| public class CapacitorCookies extends Plugin { | ||||
| 
 | ||||
|     CapacitorCookieManager cookieManager; | ||||
| 
 | ||||
|     @Override | ||||
|     public void load() { | ||||
|         this.bridge.getWebView().addJavascriptInterface(this, "CapacitorCookiesAndroidInterface"); | ||||
|         this.cookieManager = new CapacitorCookieManager(null, java.net.CookiePolicy.ACCEPT_ALL, this.bridge); | ||||
|         this.cookieManager.removeSessionCookies(); | ||||
|         CookieHandler.setDefault(this.cookieManager); | ||||
|         super.load(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void handleOnDestroy() { | ||||
|         super.handleOnDestroy(); | ||||
|         this.cookieManager.removeSessionCookies(); | ||||
|     } | ||||
| 
 | ||||
|     @JavascriptInterface | ||||
|     public boolean isEnabled() { | ||||
|         PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies"); | ||||
|         return pluginConfig.getBoolean("enabled", false); | ||||
|     } | ||||
| 
 | ||||
|     @JavascriptInterface | ||||
|     public void setCookie(String domain, String action) { | ||||
|         cookieManager.setCookie(domain, action); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void getCookies(PluginCall call) { | ||||
|         this.bridge.eval( | ||||
|                 "document.cookie", | ||||
|                 value -> { | ||||
|                     String cookies = value.substring(1, value.length() - 1); | ||||
|                     String[] cookieArray = cookies.split(";"); | ||||
| 
 | ||||
|                     JSObject cookieMap = new JSObject(); | ||||
| 
 | ||||
|                     for (String cookie : cookieArray) { | ||||
|                         if (cookie.length() > 0) { | ||||
|                             String[] keyValue = cookie.split("=", 2); | ||||
| 
 | ||||
|                             if (keyValue.length == 2) { | ||||
|                                 String key = keyValue[0].trim(); | ||||
|                                 String val = keyValue[1].trim(); | ||||
|                                 try { | ||||
|                                     key = URLDecoder.decode(keyValue[0].trim(), StandardCharsets.UTF_8.name()); | ||||
|                                     val = URLDecoder.decode(keyValue[1].trim(), StandardCharsets.UTF_8.name()); | ||||
|                                 } catch (UnsupportedEncodingException ignored) {} | ||||
| 
 | ||||
|                                 cookieMap.put(key, val); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
| 
 | ||||
|                     call.resolve(cookieMap); | ||||
|                 } | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void setCookie(PluginCall call) { | ||||
|         String key = call.getString("key"); | ||||
|         if (null == key) { | ||||
|             call.reject("Must provide key"); | ||||
|         } | ||||
|         String value = call.getString("value"); | ||||
|         if (null == value) { | ||||
|             call.reject("Must provide value"); | ||||
|         } | ||||
|         String url = call.getString("url"); | ||||
|         String expires = call.getString("expires", ""); | ||||
|         String path = call.getString("path", "/"); | ||||
|         cookieManager.setCookie(url, key, value, expires, path); | ||||
|         call.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void deleteCookie(PluginCall call) { | ||||
|         String key = call.getString("key"); | ||||
|         if (null == key) { | ||||
|             call.reject("Must provide key"); | ||||
|         } | ||||
|         String url = call.getString("url"); | ||||
|         cookieManager.setCookie(url, key + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT"); | ||||
|         call.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void clearCookies(PluginCall call) { | ||||
|         String url = call.getString("url"); | ||||
|         HttpCookie[] cookies = cookieManager.getCookies(url); | ||||
|         for (HttpCookie cookie : cookies) { | ||||
|             cookieManager.setCookie(url, cookie.getName() + "=; Expires=Wed, 31 Dec 2000 23:59:59 GMT"); | ||||
|         } | ||||
|         call.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void clearAllCookies(PluginCall call) { | ||||
|         cookieManager.removeAllCookies(); | ||||
|         call.resolve(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,119 @@ | |||
| package com.getcapacitor.plugin; | ||||
| 
 | ||||
| import android.Manifest; | ||||
| import android.webkit.JavascriptInterface; | ||||
| import com.getcapacitor.JSObject; | ||||
| import com.getcapacitor.Plugin; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import com.getcapacitor.PluginConfig; | ||||
| import com.getcapacitor.PluginMethod; | ||||
| import com.getcapacitor.annotation.CapacitorPlugin; | ||||
| import com.getcapacitor.annotation.Permission; | ||||
| import com.getcapacitor.plugin.util.CapacitorHttpUrlConnection; | ||||
| import com.getcapacitor.plugin.util.HttpRequestHandler; | ||||
| import java.util.Map; | ||||
| import java.util.concurrent.ConcurrentHashMap; | ||||
| import java.util.concurrent.ExecutorService; | ||||
| import java.util.concurrent.Executors; | ||||
| 
 | ||||
| @CapacitorPlugin( | ||||
|     permissions = { | ||||
|         @Permission(strings = { Manifest.permission.WRITE_EXTERNAL_STORAGE }, alias = "HttpWrite"), | ||||
|         @Permission(strings = { Manifest.permission.READ_EXTERNAL_STORAGE }, alias = "HttpRead") | ||||
|     } | ||||
| ) | ||||
| public class CapacitorHttp extends Plugin { | ||||
| 
 | ||||
|     private final Map<Runnable, PluginCall> activeRequests = new ConcurrentHashMap<>(); | ||||
|     private final ExecutorService executor = Executors.newCachedThreadPool(); | ||||
| 
 | ||||
|     @Override | ||||
|     public void load() { | ||||
|         this.bridge.getWebView().addJavascriptInterface(this, "CapacitorHttpAndroidInterface"); | ||||
|         super.load(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     protected void handleOnDestroy() { | ||||
|         super.handleOnDestroy(); | ||||
| 
 | ||||
|         for (Map.Entry<Runnable, PluginCall> entry : activeRequests.entrySet()) { | ||||
|             Runnable job = entry.getKey(); | ||||
|             PluginCall call = entry.getValue(); | ||||
| 
 | ||||
|             if (call.getData().has("activeCapacitorHttpUrlConnection")) { | ||||
|                 try { | ||||
|                     CapacitorHttpUrlConnection connection = (CapacitorHttpUrlConnection) call | ||||
|                         .getData() | ||||
|                         .get("activeCapacitorHttpUrlConnection"); | ||||
|                     connection.disconnect(); | ||||
|                     call.getData().remove("activeCapacitorHttpUrlConnection"); | ||||
|                 } catch (Exception ignored) {} | ||||
|             } | ||||
| 
 | ||||
|             getBridge().releaseCall(call); | ||||
|         } | ||||
| 
 | ||||
|         activeRequests.clear(); | ||||
|         executor.shutdownNow(); | ||||
|     } | ||||
| 
 | ||||
|     private void http(final PluginCall call, final String httpMethod) { | ||||
|         Runnable asyncHttpCall = new Runnable() { | ||||
|             @Override | ||||
|             public void run() { | ||||
|                 try { | ||||
|                     JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge()); | ||||
|                     call.resolve(response); | ||||
|                 } catch (Exception e) { | ||||
|                     call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e); | ||||
|                 } finally { | ||||
|                     activeRequests.remove(this); | ||||
|                 } | ||||
|             } | ||||
|         }; | ||||
| 
 | ||||
|         if (!executor.isShutdown()) { | ||||
|             activeRequests.put(asyncHttpCall, call); | ||||
|             executor.submit(asyncHttpCall); | ||||
|         } else { | ||||
|             call.reject("Failed to execute request - Http Plugin was shutdown"); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @JavascriptInterface | ||||
|     public boolean isEnabled() { | ||||
|         PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorHttp"); | ||||
|         return pluginConfig.getBoolean("enabled", false); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void request(final PluginCall call) { | ||||
|         this.http(call, null); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void get(final PluginCall call) { | ||||
|         this.http(call, "GET"); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void post(final PluginCall call) { | ||||
|         this.http(call, "POST"); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void put(final PluginCall call) { | ||||
|         this.http(call, "PUT"); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void patch(final PluginCall call) { | ||||
|         this.http(call, "PATCH"); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void delete(final PluginCall call) { | ||||
|         this.http(call, "DELETE"); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,48 @@ | |||
| package com.getcapacitor.plugin; | ||||
| 
 | ||||
| import android.app.Activity; | ||||
| import android.content.SharedPreferences; | ||||
| import com.getcapacitor.JSObject; | ||||
| import com.getcapacitor.Plugin; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import com.getcapacitor.PluginMethod; | ||||
| import com.getcapacitor.annotation.CapacitorPlugin; | ||||
| 
 | ||||
| @CapacitorPlugin | ||||
| public class WebView extends Plugin { | ||||
| 
 | ||||
|     public static final String WEBVIEW_PREFS_NAME = "CapWebViewSettings"; | ||||
|     public static final String CAP_SERVER_PATH = "serverBasePath"; | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void setServerAssetPath(PluginCall call) { | ||||
|         String path = call.getString("path"); | ||||
|         bridge.setServerAssetPath(path); | ||||
|         call.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void setServerBasePath(PluginCall call) { | ||||
|         String path = call.getString("path"); | ||||
|         bridge.setServerBasePath(path); | ||||
|         call.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void getServerBasePath(PluginCall call) { | ||||
|         String path = bridge.getServerBasePath(); | ||||
|         JSObject ret = new JSObject(); | ||||
|         ret.put("path", path); | ||||
|         call.resolve(ret); | ||||
|     } | ||||
| 
 | ||||
|     @PluginMethod | ||||
|     public void persistServerBasePath(PluginCall call) { | ||||
|         String path = bridge.getServerBasePath(); | ||||
|         SharedPreferences prefs = getContext().getSharedPreferences(WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); | ||||
|         SharedPreferences.Editor editor = prefs.edit(); | ||||
|         editor.putString(CAP_SERVER_PATH, path); | ||||
|         editor.apply(); | ||||
|         call.resolve(); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,358 @@ | |||
| package com.getcapacitor.plugin.util; | ||||
| 
 | ||||
| import android.content.ContentResolver; | ||||
| import android.content.Context; | ||||
| import android.content.res.AssetManager; | ||||
| import android.content.res.Resources; | ||||
| import android.graphics.Bitmap; | ||||
| import android.graphics.BitmapFactory; | ||||
| import android.net.Uri; | ||||
| import android.os.StrictMode; | ||||
| import androidx.core.content.FileProvider; | ||||
| import com.getcapacitor.Logger; | ||||
| import java.io.File; | ||||
| import java.io.FileNotFoundException; | ||||
| import java.io.FileOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URL; | ||||
| import java.util.UUID; | ||||
| 
 | ||||
| /** | ||||
|  * Manager for assets. | ||||
|  */ | ||||
| public final class AssetUtil { | ||||
| 
 | ||||
|     public static final int RESOURCE_ID_ZERO_VALUE = 0; | ||||
|     // Name of the storage folder | ||||
|     private static final String STORAGE_FOLDER = "/capacitorassets"; | ||||
| 
 | ||||
|     // Ref to the context passed through the constructor to access the | ||||
|     // resources and app directory. | ||||
|     private final Context context; | ||||
| 
 | ||||
|     /** | ||||
|      * Constructor | ||||
|      * | ||||
|      * @param context Application context. | ||||
|      */ | ||||
|     private AssetUtil(Context context) { | ||||
|         this.context = context; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Static method to retrieve class instance. | ||||
|      * | ||||
|      * @param context Application context. | ||||
|      */ | ||||
|     public static AssetUtil getInstance(Context context) { | ||||
|         return new AssetUtil(context); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The URI for a path. | ||||
|      * | ||||
|      * @param path The given path. | ||||
|      */ | ||||
|     public Uri parse(String path) { | ||||
|         if (path == null || path.isEmpty()) { | ||||
|             return Uri.EMPTY; | ||||
|         } else if (path.startsWith("res:")) { | ||||
|             return getUriForResourcePath(path); | ||||
|         } else if (path.startsWith("file:///")) { | ||||
|             return getUriFromPath(path); | ||||
|         } else if (path.startsWith("file://")) { | ||||
|             return getUriFromAsset(path); | ||||
|         } else if (path.startsWith("http")) { | ||||
|             return getUriFromRemote(path); | ||||
|         } else if (path.startsWith("content://")) { | ||||
|             return Uri.parse(path); | ||||
|         } | ||||
| 
 | ||||
|         return Uri.EMPTY; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * URI for a file. | ||||
|      * | ||||
|      * @param path Absolute path like file:///... | ||||
|      * | ||||
|      * @return URI pointing to the given path. | ||||
|      */ | ||||
|     private Uri getUriFromPath(String path) { | ||||
|         String absPath = path.replaceFirst("file://", "").replaceFirst("\\?.*$", ""); | ||||
|         File file = new File(absPath); | ||||
| 
 | ||||
|         if (!file.exists()) { | ||||
|             Logger.error("File not found: " + file.getAbsolutePath()); | ||||
|             return Uri.EMPTY; | ||||
|         } | ||||
| 
 | ||||
|         return getUriFromFile(file); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * URI for an asset. | ||||
|      * | ||||
|      * @param path Asset path like file://... | ||||
|      * | ||||
|      * @return URI pointing to the given path. | ||||
|      */ | ||||
|     private Uri getUriFromAsset(String path) { | ||||
|         String resPath = path.replaceFirst("file:/", "www").replaceFirst("\\?.*$", ""); | ||||
|         String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); | ||||
|         File file = getTmpFile(fileName); | ||||
| 
 | ||||
|         if (file == null) return Uri.EMPTY; | ||||
| 
 | ||||
|         try { | ||||
|             AssetManager assets = context.getAssets(); | ||||
|             InputStream in = assets.open(resPath); | ||||
|             FileOutputStream out = new FileOutputStream(file); | ||||
|             copyFile(in, out); | ||||
|         } catch (Exception e) { | ||||
|             Logger.error("File not found: assets/" + resPath); | ||||
|             return Uri.EMPTY; | ||||
|         } | ||||
| 
 | ||||
|         return getUriFromFile(file); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * The URI for a resource. | ||||
|      * | ||||
|      * @param path The given relative path. | ||||
|      * | ||||
|      * @return URI pointing to the given path. | ||||
|      */ | ||||
|     private Uri getUriForResourcePath(String path) { | ||||
|         Resources res = context.getResources(); | ||||
|         String resPath = path.replaceFirst("res://", ""); | ||||
|         int resId = getResId(resPath); | ||||
| 
 | ||||
|         if (resId == 0) { | ||||
|             Logger.error("File not found: " + resPath); | ||||
|             return Uri.EMPTY; | ||||
|         } | ||||
| 
 | ||||
|         return new Uri.Builder() | ||||
|             .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) | ||||
|             .authority(res.getResourcePackageName(resId)) | ||||
|             .appendPath(res.getResourceTypeName(resId)) | ||||
|             .appendPath(res.getResourceEntryName(resId)) | ||||
|             .build(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Uri from remote located content. | ||||
|      * | ||||
|      * @param path Remote address. | ||||
|      * | ||||
|      * @return Uri of the downloaded file. | ||||
|      */ | ||||
|     private Uri getUriFromRemote(String path) { | ||||
|         File file = getTmpFile(); | ||||
| 
 | ||||
|         if (file == null) return Uri.EMPTY; | ||||
| 
 | ||||
|         try { | ||||
|             URL url = new URL(path); | ||||
|             HttpURLConnection connection = (HttpURLConnection) url.openConnection(); | ||||
| 
 | ||||
|             StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); | ||||
| 
 | ||||
|             StrictMode.setThreadPolicy(policy); | ||||
| 
 | ||||
|             connection.setRequestProperty("Connection", "close"); | ||||
|             connection.setConnectTimeout(5000); | ||||
|             connection.connect(); | ||||
| 
 | ||||
|             InputStream in = connection.getInputStream(); | ||||
|             FileOutputStream out = new FileOutputStream(file); | ||||
| 
 | ||||
|             copyFile(in, out); | ||||
|             return getUriFromFile(file); | ||||
|         } catch (MalformedURLException e) { | ||||
|             Logger.error(Logger.tags("Asset"), "Incorrect URL", e); | ||||
|         } catch (FileNotFoundException e) { | ||||
|             Logger.error(Logger.tags("Asset"), "Failed to create new File from HTTP Content", e); | ||||
|         } catch (IOException e) { | ||||
|             Logger.error(Logger.tags("Asset"), "No Input can be created from http Stream", e); | ||||
|         } | ||||
| 
 | ||||
|         return Uri.EMPTY; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Copy content from input stream into output stream. | ||||
|      * | ||||
|      * @param in  The input stream. | ||||
|      * @param out The output stream. | ||||
|      */ | ||||
|     private void copyFile(InputStream in, FileOutputStream out) { | ||||
|         byte[] buffer = new byte[1024]; | ||||
|         int read; | ||||
| 
 | ||||
|         try { | ||||
|             while ((read = in.read(buffer)) != -1) { | ||||
|                 out.write(buffer, 0, read); | ||||
|             } | ||||
|             out.flush(); | ||||
|             out.close(); | ||||
|         } catch (Exception e) { | ||||
|             Logger.error("Error copying", e); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Resource ID for drawable. | ||||
|      * | ||||
|      * @param resPath Resource path as string. | ||||
|      * | ||||
|      * @return The resource ID or 0 if not found. | ||||
|      */ | ||||
|     public int getResId(String resPath) { | ||||
|         int resId = getResId(context.getResources(), resPath); | ||||
| 
 | ||||
|         if (resId == 0) { | ||||
|             resId = getResId(Resources.getSystem(), resPath); | ||||
|         } | ||||
| 
 | ||||
|         return resId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get resource ID. | ||||
|      * | ||||
|      * @param res     The resources where to look for. | ||||
|      * @param resPath The name of the resource. | ||||
|      * | ||||
|      * @return The resource ID or 0 if not found. | ||||
|      */ | ||||
|     private int getResId(Resources res, String resPath) { | ||||
|         String pkgName = getPkgName(res); | ||||
|         String resName = getBaseName(resPath); | ||||
|         int resId; | ||||
| 
 | ||||
|         resId = res.getIdentifier(resName, "mipmap", pkgName); | ||||
| 
 | ||||
|         if (resId == 0) { | ||||
|             resId = res.getIdentifier(resName, "drawable", pkgName); | ||||
|         } | ||||
| 
 | ||||
|         if (resId == 0) { | ||||
|             resId = res.getIdentifier(resName, "raw", pkgName); | ||||
|         } | ||||
| 
 | ||||
|         return resId; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Convert URI to Bitmap. | ||||
|      * | ||||
|      * @param uri Internal image URI | ||||
|      */ | ||||
|     public Bitmap getIconFromUri(Uri uri) throws IOException { | ||||
|         InputStream input = context.getContentResolver().openInputStream(uri); | ||||
|         return BitmapFactory.decodeStream(input); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Extract name of drawable resource from path. | ||||
|      * | ||||
|      * @param resPath Resource path as string. | ||||
|      */ | ||||
|     private String getBaseName(String resPath) { | ||||
|         String drawable = resPath; | ||||
| 
 | ||||
|         if (drawable.contains("/")) { | ||||
|             drawable = drawable.substring(drawable.lastIndexOf('/') + 1); | ||||
|         } | ||||
| 
 | ||||
|         if (resPath.contains(".")) { | ||||
|             drawable = drawable.substring(0, drawable.lastIndexOf('.')); | ||||
|         } | ||||
| 
 | ||||
|         return drawable; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a file located under the external cache dir of that app. | ||||
|      * | ||||
|      * @return File with a random UUID name. | ||||
|      */ | ||||
|     private File getTmpFile() { | ||||
|         return getTmpFile(UUID.randomUUID().toString()); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a file located under the external cache dir of that app. | ||||
|      * | ||||
|      * @param name The name of the file. | ||||
|      * | ||||
|      * @return File with the provided name. | ||||
|      */ | ||||
|     private File getTmpFile(String name) { | ||||
|         File dir = context.getExternalCacheDir(); | ||||
| 
 | ||||
|         if (dir == null) { | ||||
|             dir = context.getCacheDir(); | ||||
|         } | ||||
| 
 | ||||
|         if (dir == null) { | ||||
|             Logger.error(Logger.tags("Asset"), "Missing cache dir", null); | ||||
|             return null; | ||||
|         } | ||||
| 
 | ||||
|         String storage = dir.toString() + STORAGE_FOLDER; | ||||
| 
 | ||||
|         //noinspection ResultOfMethodCallIgnored | ||||
|         new File(storage).mkdir(); | ||||
| 
 | ||||
|         return new File(storage, name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get content URI for the specified file. | ||||
|      * | ||||
|      * @param file The file to get the URI. | ||||
|      * | ||||
|      * @return content://... | ||||
|      */ | ||||
|     private Uri getUriFromFile(File file) { | ||||
|         try { | ||||
|             String authority = context.getPackageName() + ".provider"; | ||||
|             return FileProvider.getUriForFile(context, authority, file); | ||||
|         } catch (IllegalArgumentException e) { | ||||
|             Logger.error("File not supported by provider", e); | ||||
|             return Uri.EMPTY; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Package name specified by the resource bundle. | ||||
|      */ | ||||
|     private String getPkgName(Resources res) { | ||||
|         return res == Resources.getSystem() ? "android" : context.getPackageName(); | ||||
|     } | ||||
| 
 | ||||
|     public static int getResourceID(Context context, String resourceName, String dir) { | ||||
|         return context.getResources().getIdentifier(resourceName, dir, context.getPackageName()); | ||||
|     } | ||||
| 
 | ||||
|     public static String getResourceBaseName(String resPath) { | ||||
|         if (resPath == null) return null; | ||||
| 
 | ||||
|         if (resPath.contains("/")) { | ||||
|             return resPath.substring(resPath.lastIndexOf('/') + 1); | ||||
|         } | ||||
| 
 | ||||
|         if (resPath.contains(".")) { | ||||
|             return resPath.substring(0, resPath.lastIndexOf('.')); | ||||
|         } | ||||
| 
 | ||||
|         return resPath; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,478 @@ | |||
| package com.getcapacitor.plugin.util; | ||||
| 
 | ||||
| import android.os.Build; | ||||
| import android.os.LocaleList; | ||||
| import android.text.TextUtils; | ||||
| import com.getcapacitor.Bridge; | ||||
| import com.getcapacitor.JSArray; | ||||
| import com.getcapacitor.JSObject; | ||||
| import com.getcapacitor.JSValue; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import java.io.DataOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.lang.reflect.Method; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.ProtocolException; | ||||
| import java.net.SocketTimeoutException; | ||||
| import java.net.URL; | ||||
| import java.net.URLEncoder; | ||||
| import java.net.UnknownServiceException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.Base64; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import javax.net.ssl.HttpsURLConnection; | ||||
| import javax.net.ssl.SSLSocketFactory; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| public class CapacitorHttpUrlConnection implements ICapacitorHttpUrlConnection { | ||||
| 
 | ||||
|     private final HttpURLConnection connection; | ||||
| 
 | ||||
|     /** | ||||
|      * Make a new CapacitorHttpUrlConnection instance, which wraps around HttpUrlConnection | ||||
|      * and provides some helper functions for setting request headers and the request body | ||||
|      * @param conn the base HttpUrlConnection. You can pass the value from | ||||
|      *             {@code (HttpUrlConnection) URL.openConnection()} | ||||
|      */ | ||||
|     public CapacitorHttpUrlConnection(HttpURLConnection conn) { | ||||
|         connection = conn; | ||||
|         this.setDefaultRequestProperties(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the underlying HttpUrlConnection value | ||||
|      * @return the underlying HttpUrlConnection value | ||||
|      */ | ||||
|     public HttpURLConnection getHttpConnection() { | ||||
|         return connection; | ||||
|     } | ||||
| 
 | ||||
|     public void disconnect() { | ||||
|         connection.disconnect(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the value of the {@code allowUserInteraction} field of | ||||
|      * this {@code URLConnection}. | ||||
|      * | ||||
|      * @param   isAllowedInteraction   the new value. | ||||
|      * @throws IllegalStateException if already connected | ||||
|      */ | ||||
|     public void setAllowUserInteraction(boolean isAllowedInteraction) { | ||||
|         connection.setAllowUserInteraction(isAllowedInteraction); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Set the method for the URL request, one of: | ||||
|      * <UL> | ||||
|      *  <LI>GET | ||||
|      *  <LI>POST | ||||
|      *  <LI>HEAD | ||||
|      *  <LI>OPTIONS | ||||
|      *  <LI>PUT | ||||
|      *  <LI>DELETE | ||||
|      *  <LI>TRACE | ||||
|      * </UL> are legal, subject to protocol restrictions.  The default | ||||
|      * method is GET. | ||||
|      * | ||||
|      * @param method the HTTP method | ||||
|      * @exception ProtocolException if the method cannot be reset or if | ||||
|      *              the requested method isn't valid for HTTP. | ||||
|      * @exception SecurityException if a security manager is set and the | ||||
|      *              method is "TRACE", but the "allowHttpTrace" | ||||
|      *              NetPermission is not granted. | ||||
|      */ | ||||
|     public void setRequestMethod(String method) throws ProtocolException { | ||||
|         connection.setRequestMethod(method); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets a specified timeout value, in milliseconds, to be used | ||||
|      * when opening a communications link to the resource referenced | ||||
|      * by this URLConnection.  If the timeout expires before the | ||||
|      * connection can be established, a | ||||
|      * java.net.SocketTimeoutException is raised. A timeout of zero is | ||||
|      * interpreted as an infinite timeout. | ||||
|      * | ||||
|      * <p><strong>Warning</strong>: If the hostname resolves to multiple IP | ||||
|      * addresses, Android's default implementation of {@link HttpURLConnection} | ||||
|      * will try each in | ||||
|      * <a href="http://www.ietf.org/rfc/rfc3484.txt">RFC 3484</a> order. If | ||||
|      * connecting to each of these addresses fails, multiple timeouts will | ||||
|      * elapse before the connect attempt throws an exception. Host names | ||||
|      * that support both IPv6 and IPv4 always have at least 2 IP addresses. | ||||
|      * | ||||
|      * @param timeout an {@code int} that specifies the connect | ||||
|      *               timeout value in milliseconds | ||||
|      * @throws IllegalArgumentException if the timeout parameter is negative | ||||
|      */ | ||||
|     public void setConnectTimeout(int timeout) { | ||||
|         if (timeout < 0) { | ||||
|             throw new IllegalArgumentException("timeout can not be negative"); | ||||
|         } | ||||
|         connection.setConnectTimeout(timeout); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the read timeout to a specified timeout, in | ||||
|      * milliseconds. A non-zero value specifies the timeout when | ||||
|      * reading from Input stream when a connection is established to a | ||||
|      * resource. If the timeout expires before there is data available | ||||
|      * for read, a java.net.SocketTimeoutException is raised. A | ||||
|      * timeout of zero is interpreted as an infinite timeout. | ||||
|      * | ||||
|      * @param timeout an {@code int} that specifies the timeout | ||||
|      * value to be used in milliseconds | ||||
|      * @throws IllegalArgumentException if the timeout parameter is negative | ||||
|      */ | ||||
|     public void setReadTimeout(int timeout) { | ||||
|         if (timeout < 0) { | ||||
|             throw new IllegalArgumentException("timeout can not be negative"); | ||||
|         } | ||||
|         connection.setReadTimeout(timeout); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets whether automatic HTTP redirects should be disabled | ||||
|      * @param disableRedirects the flag to determine if redirects should be followed | ||||
|      */ | ||||
|     public void setDisableRedirects(boolean disableRedirects) { | ||||
|         connection.setInstanceFollowRedirects(!disableRedirects); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the request headers given a JSObject of key-value pairs | ||||
|      * @param headers the JSObject values to map to the HttpUrlConnection request headers | ||||
|      */ | ||||
|     public void setRequestHeaders(JSObject headers) { | ||||
|         Iterator<String> keys = headers.keys(); | ||||
|         while (keys.hasNext()) { | ||||
|             String key = keys.next(); | ||||
|             String value = headers.getString(key); | ||||
|             connection.setRequestProperty(key, value); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the value of the {@code doOutput} field for this | ||||
|      * {@code URLConnection} to the specified value. | ||||
|      * <p> | ||||
|      * A URL connection can be used for input and/or output.  Set the DoOutput | ||||
|      * flag to true if you intend to use the URL connection for output, | ||||
|      * false if not.  The default is false. | ||||
|      * | ||||
|      * @param  shouldDoOutput   the new value. | ||||
|      * @throws IllegalStateException if already connected | ||||
|      */ | ||||
|     public void setDoOutput(boolean shouldDoOutput) { | ||||
|         connection.setDoOutput(shouldDoOutput); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * @param call | ||||
|      * @throws JSONException | ||||
|      * @throws IOException | ||||
|      */ | ||||
|     public void setRequestBody(PluginCall call, JSValue body) throws JSONException, IOException { | ||||
|         setRequestBody(call, body, null); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * | ||||
|      * @param call | ||||
|      * @throws JSONException | ||||
|      * @throws IOException | ||||
|      */ | ||||
|     public void setRequestBody(PluginCall call, JSValue body, String bodyType) throws JSONException, IOException { | ||||
|         String contentType = connection.getRequestProperty("Content-Type"); | ||||
|         String dataString = ""; | ||||
| 
 | ||||
|         if (contentType == null || contentType.isEmpty()) return; | ||||
| 
 | ||||
|         if (contentType.contains("application/json")) { | ||||
|             JSArray jsArray = null; | ||||
|             if (body != null) { | ||||
|                 dataString = body.toString(); | ||||
|             } else { | ||||
|                 jsArray = call.getArray("data", null); | ||||
|             } | ||||
|             if (jsArray != null) { | ||||
|                 dataString = jsArray.toString(); | ||||
|             } else if (body == null) { | ||||
|                 dataString = call.getString("data"); | ||||
|             } | ||||
|             this.writeRequestBody(dataString != null ? dataString : ""); | ||||
|         } else if (bodyType != null && bodyType.equals("file")) { | ||||
|             try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | ||||
|                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                     os.write(Base64.getDecoder().decode(body.toString())); | ||||
|                 } | ||||
|                 os.flush(); | ||||
|             } | ||||
|         } else if (contentType.contains("application/x-www-form-urlencoded")) { | ||||
|             try { | ||||
|                 JSObject obj = body.toJSObject(); | ||||
|                 this.writeObjectRequestBody(obj); | ||||
|             } catch (Exception e) { | ||||
|                 // Body is not a valid JSON, treat it as an already formatted string | ||||
|                 this.writeRequestBody(body.toString()); | ||||
|             } | ||||
|         } else if (bodyType != null && bodyType.equals("formData")) { | ||||
|             this.writeFormDataRequestBody(contentType, body.toJSArray()); | ||||
|         } else { | ||||
|             this.writeRequestBody(body.toString()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Writes the provided string to the HTTP connection managed by this instance. | ||||
|      * | ||||
|      * @param body The string value to write to the connection stream. | ||||
|      */ | ||||
|     private void writeRequestBody(String body) throws IOException { | ||||
|         try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | ||||
|             os.write(body.getBytes(StandardCharsets.UTF_8)); | ||||
|             os.flush(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void writeObjectRequestBody(JSObject object) throws IOException, JSONException { | ||||
|         try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | ||||
|             Iterator<String> keys = object.keys(); | ||||
|             while (keys.hasNext()) { | ||||
|                 String key = keys.next(); | ||||
|                 Object d = object.get(key); | ||||
|                 os.writeBytes(key); | ||||
|                 os.writeBytes("="); | ||||
|                 os.writeBytes(URLEncoder.encode(d.toString(), "UTF-8")); | ||||
| 
 | ||||
|                 if (keys.hasNext()) { | ||||
|                     os.writeBytes("&"); | ||||
|                 } | ||||
|             } | ||||
|             os.flush(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void writeFormDataRequestBody(String contentType, JSArray entries) throws IOException, JSONException { | ||||
|         try (DataOutputStream os = new DataOutputStream(connection.getOutputStream())) { | ||||
|             String boundary = contentType.split(";")[1].split("=")[1]; | ||||
|             String lineEnd = "\r\n"; | ||||
|             String twoHyphens = "--"; | ||||
| 
 | ||||
|             for (Object e : entries.toList()) { | ||||
|                 if (e instanceof JSONObject) { | ||||
|                     JSONObject entry = (JSONObject) e; | ||||
|                     String type = entry.getString("type"); | ||||
|                     String key = entry.getString("key"); | ||||
|                     String value = entry.getString("value"); | ||||
|                     if (type.equals("string")) { | ||||
|                         os.writeBytes(twoHyphens + boundary + lineEnd); | ||||
|                         os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"" + lineEnd + lineEnd); | ||||
|                         os.write(value.getBytes(StandardCharsets.UTF_8)); | ||||
|                         os.writeBytes(lineEnd); | ||||
|                     } else if (type.equals("base64File")) { | ||||
|                         String fileName = entry.getString("fileName"); | ||||
|                         String fileContentType = entry.getString("contentType"); | ||||
| 
 | ||||
|                         os.writeBytes(twoHyphens + boundary + lineEnd); | ||||
|                         os.writeBytes("Content-Disposition: form-data; name=\"" + key + "\"; filename=\"" + fileName + "\"" + lineEnd); | ||||
|                         os.writeBytes("Content-Type: " + fileContentType + lineEnd); | ||||
|                         os.writeBytes("Content-Transfer-Encoding: binary" + lineEnd); | ||||
|                         os.writeBytes(lineEnd); | ||||
| 
 | ||||
|                         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { | ||||
|                             os.write(Base64.getDecoder().decode(value)); | ||||
|                         } else { | ||||
|                             os.write(android.util.Base64.decode(value, android.util.Base64.DEFAULT)); | ||||
|                         } | ||||
| 
 | ||||
|                         os.writeBytes(lineEnd); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             os.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd); | ||||
|             os.flush(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Opens a communications link to the resource referenced by this | ||||
|      * URL, if such a connection has not already been established. | ||||
|      * <p> | ||||
|      * If the {@code connect} method is called when the connection | ||||
|      * has already been opened (indicated by the {@code connected} | ||||
|      * field having the value {@code true}), the call is ignored. | ||||
|      * <p> | ||||
|      * URLConnection objects go through two phases: first they are | ||||
|      * created, then they are connected.  After being created, and | ||||
|      * before being connected, various options can be specified | ||||
|      * (e.g., doInput and UseCaches).  After connecting, it is an | ||||
|      * error to try to set them.  Operations that depend on being | ||||
|      * connected, like getContentLength, will implicitly perform the | ||||
|      * connection, if necessary. | ||||
|      * | ||||
|      * @throws SocketTimeoutException if the timeout expires before | ||||
|      *               the connection can be established | ||||
|      * @exception  IOException  if an I/O error occurs while opening the | ||||
|      *               connection. | ||||
|      */ | ||||
|     public void connect() throws IOException { | ||||
|         connection.connect(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Gets the status code from an HTTP response message. | ||||
|      * For example, in the case of the following status lines: | ||||
|      * <PRE> | ||||
|      * HTTP/1.0 200 OK | ||||
|      * HTTP/1.0 401 Unauthorized | ||||
|      * </PRE> | ||||
|      * It will return 200 and 401 respectively. | ||||
|      * Returns -1 if no code can be discerned | ||||
|      * from the response (i.e., the response is not valid HTTP). | ||||
|      * @throws IOException if an error occurred connecting to the server. | ||||
|      * @return the HTTP Status-Code, or -1 | ||||
|      */ | ||||
|     public int getResponseCode() throws IOException { | ||||
|         return connection.getResponseCode(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the value of this {@code URLConnection}'s {@code URL} | ||||
|      * field. | ||||
|      * | ||||
|      * @return  the value of this {@code URLConnection}'s {@code URL} | ||||
|      *          field. | ||||
|      */ | ||||
|     public URL getURL() { | ||||
|         return connection.getURL(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the error stream if the connection failed | ||||
|      * but the server sent useful data nonetheless. The | ||||
|      * typical example is when an HTTP server responds | ||||
|      * with a 404, which will cause a FileNotFoundException | ||||
|      * to be thrown in connect, but the server sent an HTML | ||||
|      * help page with suggestions as to what to do. | ||||
|      * | ||||
|      * <p>This method will not cause a connection to be initiated.  If | ||||
|      * the connection was not connected, or if the server did not have | ||||
|      * an error while connecting or if the server had an error but | ||||
|      * no error data was sent, this method will return null. This is | ||||
|      * the default. | ||||
|      * | ||||
|      * @return an error stream if any, null if there have been no | ||||
|      * errors, the connection is not connected or the server sent no | ||||
|      * useful data. | ||||
|      */ | ||||
|     @Override | ||||
|     public InputStream getErrorStream() { | ||||
|         return connection.getErrorStream(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns the value of the named header field. | ||||
|      * <p> | ||||
|      * If called on a connection that sets the same header multiple times | ||||
|      * with possibly different values, only the last value is returned. | ||||
|      * | ||||
|      * | ||||
|      * @param   name   the name of a header field. | ||||
|      * @return  the value of the named header field, or {@code null} | ||||
|      *          if there is no such field in the header. | ||||
|      */ | ||||
|     @Override | ||||
|     public String getHeaderField(String name) { | ||||
|         return connection.getHeaderField(name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an input stream that reads from this open connection. | ||||
|      * | ||||
|      * A SocketTimeoutException can be thrown when reading from the | ||||
|      * returned input stream if the read timeout expires before data | ||||
|      * is available for read. | ||||
|      * | ||||
|      * @return     an input stream that reads from this open connection. | ||||
|      * @exception  IOException              if an I/O error occurs while | ||||
|      *               creating the input stream. | ||||
|      * @exception UnknownServiceException  if the protocol does not support | ||||
|      *               input. | ||||
|      * @see #setReadTimeout(int) | ||||
|      */ | ||||
|     @Override | ||||
|     public InputStream getInputStream() throws IOException { | ||||
|         return connection.getInputStream(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns an unmodifiable Map of the header fields. | ||||
|      * The Map keys are Strings that represent the | ||||
|      * response-header field names. Each Map value is an | ||||
|      * unmodifiable List of Strings that represents | ||||
|      * the corresponding field values. | ||||
|      * | ||||
|      * @return a Map of header fields | ||||
|      */ | ||||
|     public Map<String, List<String>> getHeaderFields() { | ||||
|         return connection.getHeaderFields(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the default request properties on the newly created connection. | ||||
|      * This is called as early as possible to allow overrides by user-provided values. | ||||
|      */ | ||||
|     private void setDefaultRequestProperties() { | ||||
|         String acceptLanguage = buildDefaultAcceptLanguageProperty(); | ||||
|         if (!TextUtils.isEmpty(acceptLanguage)) { | ||||
|             connection.setRequestProperty("Accept-Language", acceptLanguage); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds and returns a locale string describing the device's current locale preferences. | ||||
|      */ | ||||
|     private String buildDefaultAcceptLanguageProperty() { | ||||
|         Locale locale; | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | ||||
|             locale = LocaleList.getDefault().get(0); | ||||
|         } else { | ||||
|             locale = Locale.getDefault(); | ||||
|         } | ||||
|         String result = ""; | ||||
|         String lang = locale.getLanguage(); | ||||
|         String country = locale.getCountry(); | ||||
|         if (!TextUtils.isEmpty(lang)) { | ||||
|             if (!TextUtils.isEmpty(country)) { | ||||
|                 result = String.format("%s-%s,%s;q=0.5", lang, country, lang); | ||||
|             } else { | ||||
|                 result = String.format("%s;q=0.5", lang); | ||||
|             } | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
| 
 | ||||
|     public void setSSLSocketFactory(Bridge bridge) { | ||||
|         // Attach SSL Certificates if Enterprise Plugin is available | ||||
|         try { | ||||
|             Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning"); | ||||
|             Method method = sslPinningImpl.getDeclaredMethod("getSSLSocketFactory", Bridge.class); | ||||
|             SSLSocketFactory sslSocketFactory = (SSLSocketFactory) method.invoke( | ||||
|                 sslPinningImpl.getDeclaredConstructor().newInstance(), | ||||
|                 bridge | ||||
|             ); | ||||
|             if (sslSocketFactory != null) { | ||||
|                 ((HttpsURLConnection) this.connection).setSSLSocketFactory(sslSocketFactory); | ||||
|             } | ||||
|         } catch (Exception ignored) {} | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,452 @@ | |||
| package com.getcapacitor.plugin.util; | ||||
| 
 | ||||
| import android.text.TextUtils; | ||||
| import android.util.Base64; | ||||
| import com.getcapacitor.Bridge; | ||||
| import com.getcapacitor.JSArray; | ||||
| import com.getcapacitor.JSObject; | ||||
| import com.getcapacitor.JSValue; | ||||
| import com.getcapacitor.PluginCall; | ||||
| import java.io.BufferedReader; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.io.InputStreamReader; | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.lang.reflect.Method; | ||||
| import java.net.HttpURLConnection; | ||||
| import java.net.MalformedURLException; | ||||
| import java.net.URI; | ||||
| import java.net.URISyntaxException; | ||||
| import java.net.URL; | ||||
| import java.net.URLEncoder; | ||||
| import java.util.Iterator; | ||||
| import java.util.List; | ||||
| import java.util.Locale; | ||||
| import java.util.Map; | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| public class HttpRequestHandler { | ||||
| 
 | ||||
|     /** | ||||
|      * An enum specifying conventional HTTP Response Types | ||||
|      * See https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType | ||||
|      */ | ||||
|     public enum ResponseType { | ||||
|         ARRAY_BUFFER("arraybuffer"), | ||||
|         BLOB("blob"), | ||||
|         DOCUMENT("document"), | ||||
|         JSON("json"), | ||||
|         TEXT("text"); | ||||
| 
 | ||||
|         private final String name; | ||||
| 
 | ||||
|         ResponseType(String name) { | ||||
|             this.name = name; | ||||
|         } | ||||
| 
 | ||||
|         static final ResponseType DEFAULT = TEXT; | ||||
| 
 | ||||
|         public static ResponseType parse(String value) { | ||||
|             for (ResponseType responseType : values()) { | ||||
|                 if (responseType.name.equalsIgnoreCase(value)) { | ||||
|                     return responseType; | ||||
|                 } | ||||
|             } | ||||
|             return DEFAULT; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Internal builder class for building a CapacitorHttpUrlConnection | ||||
|      */ | ||||
|     public static class HttpURLConnectionBuilder { | ||||
| 
 | ||||
|         public Integer connectTimeout; | ||||
|         public Integer readTimeout; | ||||
|         public Boolean disableRedirects; | ||||
|         public JSObject headers; | ||||
|         public String method; | ||||
|         public URL url; | ||||
| 
 | ||||
|         public CapacitorHttpUrlConnection connection; | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setConnectTimeout(Integer connectTimeout) { | ||||
|             this.connectTimeout = connectTimeout; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setReadTimeout(Integer readTimeout) { | ||||
|             this.readTimeout = readTimeout; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setDisableRedirects(Boolean disableRedirects) { | ||||
|             this.disableRedirects = disableRedirects; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setHeaders(JSObject headers) { | ||||
|             this.headers = headers; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setMethod(String method) { | ||||
|             this.method = method; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setUrl(URL url) { | ||||
|             this.url = url; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder openConnection() throws IOException { | ||||
|             connection = new CapacitorHttpUrlConnection((HttpURLConnection) url.openConnection()); | ||||
| 
 | ||||
|             connection.setAllowUserInteraction(false); | ||||
|             connection.setRequestMethod(method); | ||||
| 
 | ||||
|             if (connectTimeout != null) connection.setConnectTimeout(connectTimeout); | ||||
|             if (readTimeout != null) connection.setReadTimeout(readTimeout); | ||||
|             if (disableRedirects != null) connection.setDisableRedirects(disableRedirects); | ||||
| 
 | ||||
|             connection.setRequestHeaders(headers); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setUrlParams(JSObject params) throws MalformedURLException, URISyntaxException, JSONException { | ||||
|             return this.setUrlParams(params, true); | ||||
|         } | ||||
| 
 | ||||
|         public HttpURLConnectionBuilder setUrlParams(JSObject params, boolean shouldEncode) | ||||
|             throws URISyntaxException, MalformedURLException { | ||||
|             String initialQuery = url.getQuery(); | ||||
|             String initialQueryBuilderStr = initialQuery == null ? "" : initialQuery; | ||||
| 
 | ||||
|             Iterator<String> keys = params.keys(); | ||||
| 
 | ||||
|             if (!keys.hasNext()) { | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             StringBuilder urlQueryBuilder = new StringBuilder(initialQueryBuilderStr); | ||||
| 
 | ||||
|             // Build the new query string | ||||
|             while (keys.hasNext()) { | ||||
|                 String key = keys.next(); | ||||
| 
 | ||||
|                 // Attempt as JSONArray and fallback to string if it fails | ||||
|                 try { | ||||
|                     StringBuilder value = new StringBuilder(); | ||||
|                     JSONArray arr = params.getJSONArray(key); | ||||
|                     for (int x = 0; x < arr.length(); x++) { | ||||
|                         this.addUrlParam(value, key, arr.getString(x), shouldEncode); | ||||
|                         if (x != arr.length() - 1) { | ||||
|                             value.append("&"); | ||||
|                         } | ||||
|                     } | ||||
|                     if (urlQueryBuilder.length() > 0) { | ||||
|                         urlQueryBuilder.append("&"); | ||||
|                     } | ||||
|                     urlQueryBuilder.append(value); | ||||
|                 } catch (JSONException e) { | ||||
|                     if (urlQueryBuilder.length() > 0) { | ||||
|                         urlQueryBuilder.append("&"); | ||||
|                     } | ||||
|                     this.addUrlParam(urlQueryBuilder, key, params.getString(key), shouldEncode); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             String urlQuery = urlQueryBuilder.toString(); | ||||
| 
 | ||||
|             URI uri = url.toURI(); | ||||
|             String unEncodedUrlString = | ||||
|                 uri.getScheme() + | ||||
|                 "://" + | ||||
|                 uri.getAuthority() + | ||||
|                 uri.getPath() + | ||||
|                 ((!urlQuery.equals("")) ? "?" + urlQuery : "") + | ||||
|                 ((uri.getFragment() != null) ? uri.getFragment() : ""); | ||||
|             this.url = new URL(unEncodedUrlString); | ||||
| 
 | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         private static void addUrlParam(StringBuilder sb, String key, String value, boolean shouldEncode) { | ||||
|             if (shouldEncode) { | ||||
|                 try { | ||||
|                     key = URLEncoder.encode(key, "UTF-8"); | ||||
|                     value = URLEncoder.encode(value, "UTF-8"); | ||||
|                 } catch (UnsupportedEncodingException ex) { | ||||
|                     throw new RuntimeException(ex.getCause()); | ||||
|                 } | ||||
|             } | ||||
|             sb.append(key).append("=").append(value); | ||||
|         } | ||||
| 
 | ||||
|         public CapacitorHttpUrlConnection build() { | ||||
|             return connection; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects. | ||||
|      *   Defaults to ResponseType.DEFAULT | ||||
|      * @param connection The CapacitorHttpUrlConnection to respond with | ||||
|      * @throws IOException Thrown if the InputStream is unable to be parsed correctly | ||||
|      * @throws JSONException Thrown if the JSON is unable to be parsed | ||||
|      */ | ||||
|     public static JSObject buildResponse(CapacitorHttpUrlConnection connection) throws IOException, JSONException { | ||||
|         return buildResponse(connection, ResponseType.DEFAULT); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Builds an HTTP Response given CapacitorHttpUrlConnection and ResponseType objects | ||||
|      * @param connection The CapacitorHttpUrlConnection to respond with | ||||
|      * @param responseType The requested ResponseType | ||||
|      * @return A JSObject that contains the HTTPResponse to return to the browser | ||||
|      * @throws IOException Thrown if the InputStream is unable to be parsed correctly | ||||
|      * @throws JSONException Thrown if the JSON is unable to be parsed | ||||
|      */ | ||||
|     public static JSObject buildResponse(CapacitorHttpUrlConnection connection, ResponseType responseType) | ||||
|         throws IOException, JSONException { | ||||
|         int statusCode = connection.getResponseCode(); | ||||
| 
 | ||||
|         JSObject output = new JSObject(); | ||||
|         output.put("status", statusCode); | ||||
|         output.put("headers", buildResponseHeaders(connection)); | ||||
|         output.put("url", connection.getURL()); | ||||
|         output.put("data", readData(connection, responseType)); | ||||
| 
 | ||||
|         InputStream errorStream = connection.getErrorStream(); | ||||
|         if (errorStream != null) { | ||||
|             output.put("error", true); | ||||
|         } | ||||
| 
 | ||||
|         return output; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Read the existing ICapacitorHttpUrlConnection data | ||||
|      * @param connection The ICapacitorHttpUrlConnection object to read in | ||||
|      * @param responseType The type of HTTP response to return to the API | ||||
|      * @return The parsed data from the connection | ||||
|      * @throws IOException Thrown if the InputStreams cannot be properly parsed | ||||
|      * @throws JSONException Thrown if the JSON is malformed when parsing as JSON | ||||
|      */ | ||||
|     public static Object readData(ICapacitorHttpUrlConnection connection, ResponseType responseType) throws IOException, JSONException { | ||||
|         InputStream errorStream = connection.getErrorStream(); | ||||
|         String contentType = connection.getHeaderField("Content-Type"); | ||||
| 
 | ||||
|         if (errorStream != null) { | ||||
|             if (isOneOf(contentType, MimeType.APPLICATION_JSON, MimeType.APPLICATION_VND_API_JSON)) { | ||||
|                 return parseJSON(readStreamAsString(errorStream)); | ||||
|             } else { | ||||
|                 return readStreamAsString(errorStream); | ||||
|             } | ||||
|         } else if (contentType != null && contentType.contains(MimeType.APPLICATION_JSON.getValue())) { | ||||
|             // backward compatibility | ||||
|             return parseJSON(readStreamAsString(connection.getInputStream())); | ||||
|         } else { | ||||
|             InputStream stream = connection.getInputStream(); | ||||
|             switch (responseType) { | ||||
|                 case ARRAY_BUFFER: | ||||
|                 case BLOB: | ||||
|                     return readStreamAsBase64(stream); | ||||
|                 case JSON: | ||||
|                     return parseJSON(readStreamAsString(stream)); | ||||
|                 case DOCUMENT: | ||||
|                 case TEXT: | ||||
|                 default: | ||||
|                     return readStreamAsString(stream); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Helper function for determining if the Content-Type is a typeof an existing Mime-Type | ||||
|      * @param contentType The Content-Type string to check for | ||||
|      * @param mimeTypes The Mime-Type values to check against | ||||
|      * @return | ||||
|      */ | ||||
|     public static boolean isOneOf(String contentType, MimeType... mimeTypes) { | ||||
|         if (contentType != null) { | ||||
|             for (MimeType mimeType : mimeTypes) { | ||||
|                 if (contentType.contains(mimeType.getValue())) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Build the JSObject response headers based on the connection header map | ||||
|      * @param connection The CapacitorHttpUrlConnection connection | ||||
|      * @return A JSObject of the header values from the CapacitorHttpUrlConnection | ||||
|      */ | ||||
|     public static JSObject buildResponseHeaders(CapacitorHttpUrlConnection connection) { | ||||
|         JSObject output = new JSObject(); | ||||
| 
 | ||||
|         for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) { | ||||
|             String valuesString = TextUtils.join(", ", entry.getValue()); | ||||
|             output.put(entry.getKey(), valuesString); | ||||
|         } | ||||
| 
 | ||||
|         return output; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a JSObject or a JSArray based on a string-ified input | ||||
|      * @param input String-ified JSON that needs parsing | ||||
|      * @return A JSObject or JSArray | ||||
|      * @throws JSONException thrown if the JSON is malformed | ||||
|      */ | ||||
|     public static Object parseJSON(String input) throws JSONException { | ||||
|         JSONObject json = new JSONObject(); | ||||
|         try { | ||||
|             if ("null".equals(input.trim())) { | ||||
|                 return JSONObject.NULL; | ||||
|             } else if ("true".equals(input.trim())) { | ||||
|                 return true; | ||||
|             } else if ("false".equals(input.trim())) { | ||||
|                 return false; | ||||
|             } else if (input.trim().length() <= 0) { | ||||
|                 return ""; | ||||
|             } else if (input.trim().matches("^\".*\"$")) { | ||||
|                 // a string enclosed in " " is a json value, return the string without the quotes | ||||
|                 return input.trim().substring(1, input.trim().length() - 1); | ||||
|             } else if (input.trim().matches("^-?\\d+$")) { | ||||
|                 return Integer.parseInt(input.trim()); | ||||
|             } else if (input.trim().matches("^-?\\d+(\\.\\d+)?$")) { | ||||
|                 return Double.parseDouble(input.trim()); | ||||
|             } else { | ||||
|                 try { | ||||
|                     return new JSObject(input); | ||||
|                 } catch (JSONException e) { | ||||
|                     return new JSArray(input); | ||||
|                 } | ||||
|             } | ||||
|         } catch (JSONException e) { | ||||
|             return input; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a string based on a base64 InputStream | ||||
|      * @param in The base64 InputStream to convert to a String | ||||
|      * @return String value of InputStream | ||||
|      * @throws IOException thrown if the InputStream is unable to be read as base64 | ||||
|      */ | ||||
|     public static String readStreamAsBase64(InputStream in) throws IOException { | ||||
|         try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { | ||||
|             byte[] buffer = new byte[1024]; | ||||
|             int readBytes; | ||||
|             while ((readBytes = in.read(buffer)) != -1) { | ||||
|                 out.write(buffer, 0, readBytes); | ||||
|             } | ||||
|             byte[] result = out.toByteArray(); | ||||
|             return Base64.encodeToString(result, 0, result.length, Base64.DEFAULT); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a string based on an InputStream | ||||
|      * @param in The InputStream to convert to a String | ||||
|      * @return String value of InputStream | ||||
|      * @throws IOException thrown if the InputStream is unable to be read | ||||
|      */ | ||||
|     public static String readStreamAsString(InputStream in) throws IOException { | ||||
|         try (BufferedReader reader = new BufferedReader(new InputStreamReader(in))) { | ||||
|             StringBuilder builder = new StringBuilder(); | ||||
|             String line = reader.readLine(); | ||||
|             while (line != null) { | ||||
|                 builder.append(line); | ||||
|                 line = reader.readLine(); | ||||
|                 if (line != null) { | ||||
|                     builder.append(System.getProperty("line.separator")); | ||||
|                 } | ||||
|             } | ||||
|             return builder.toString(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Makes an Http Request based on the PluginCall parameters | ||||
|      * @param call The Capacitor PluginCall that contains the options need for an Http request | ||||
|      * @param httpMethod The HTTP method that overrides the PluginCall HTTP method | ||||
|      * @throws IOException throws an IO request when a connection can't be made | ||||
|      * @throws URISyntaxException thrown when the URI is malformed | ||||
|      * @throws JSONException thrown when the incoming JSON is malformed | ||||
|      */ | ||||
|     public static JSObject request(PluginCall call, String httpMethod, Bridge bridge) | ||||
|         throws IOException, URISyntaxException, JSONException { | ||||
|         String urlString = call.getString("url", ""); | ||||
|         JSObject headers = call.getObject("headers", new JSObject()); | ||||
|         JSObject params = call.getObject("params", new JSObject()); | ||||
|         Integer connectTimeout = call.getInt("connectTimeout"); | ||||
|         Integer readTimeout = call.getInt("readTimeout"); | ||||
|         Boolean disableRedirects = call.getBoolean("disableRedirects"); | ||||
|         Boolean shouldEncode = call.getBoolean("shouldEncodeUrlParams", true); | ||||
|         ResponseType responseType = ResponseType.parse(call.getString("responseType")); | ||||
|         String dataType = call.getString("dataType"); | ||||
| 
 | ||||
|         String method = httpMethod != null ? httpMethod.toUpperCase(Locale.ROOT) : call.getString("method", "GET").toUpperCase(Locale.ROOT); | ||||
| 
 | ||||
|         boolean isHttpMutate = method.equals("DELETE") || method.equals("PATCH") || method.equals("POST") || method.equals("PUT"); | ||||
| 
 | ||||
|         URL url = new URL(urlString); | ||||
|         HttpURLConnectionBuilder connectionBuilder = new HttpURLConnectionBuilder() | ||||
|             .setUrl(url) | ||||
|             .setMethod(method) | ||||
|             .setHeaders(headers) | ||||
|             .setUrlParams(params, shouldEncode) | ||||
|             .setConnectTimeout(connectTimeout) | ||||
|             .setReadTimeout(readTimeout) | ||||
|             .setDisableRedirects(disableRedirects) | ||||
|             .openConnection(); | ||||
| 
 | ||||
|         CapacitorHttpUrlConnection connection = connectionBuilder.build(); | ||||
| 
 | ||||
|         if (null != bridge && !isDomainExcludedFromSSL(bridge, url)) { | ||||
|             connection.setSSLSocketFactory(bridge); | ||||
|         } | ||||
| 
 | ||||
|         // Set HTTP body on a non GET or HEAD request | ||||
|         if (isHttpMutate) { | ||||
|             JSValue data = new JSValue(call, "data"); | ||||
|             if (data.getValue() != null) { | ||||
|                 connection.setDoOutput(true); | ||||
|                 connection.setRequestBody(call, data, dataType); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         call.getData().put("activeCapacitorHttpUrlConnection", connection); | ||||
|         connection.connect(); | ||||
| 
 | ||||
|         JSObject response = buildResponse(connection, responseType); | ||||
| 
 | ||||
|         connection.disconnect(); | ||||
|         call.getData().remove("activeCapacitorHttpUrlConnection"); | ||||
| 
 | ||||
|         return response; | ||||
|     } | ||||
| 
 | ||||
|     public static Boolean isDomainExcludedFromSSL(Bridge bridge, URL url) { | ||||
|         try { | ||||
|             Class<?> sslPinningImpl = Class.forName("io.ionic.sslpinning.SSLPinning"); | ||||
|             Method method = sslPinningImpl.getDeclaredMethod("isDomainExcluded", Bridge.class, URL.class); | ||||
|             return (Boolean) method.invoke(sslPinningImpl.getDeclaredConstructor().newInstance(), bridge, url); | ||||
|         } catch (Exception ignored) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @FunctionalInterface | ||||
|     public interface ProgressEmitter { | ||||
|         void emit(Integer bytes, Integer contentLength); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,15 @@ | |||
| package com.getcapacitor.plugin.util; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| 
 | ||||
| /** | ||||
|  * This interface was extracted from {@link CapacitorHttpUrlConnection} to enable mocking that class. | ||||
|  */ | ||||
| public interface ICapacitorHttpUrlConnection { | ||||
|     InputStream getErrorStream(); | ||||
| 
 | ||||
|     String getHeaderField(String name); | ||||
| 
 | ||||
|     InputStream getInputStream() throws IOException; | ||||
| } | ||||
|  | @ -0,0 +1,17 @@ | |||
| package com.getcapacitor.plugin.util; | ||||
| 
 | ||||
| enum MimeType { | ||||
|     APPLICATION_JSON("application/json"), | ||||
|     APPLICATION_VND_API_JSON("application/vnd.api+json"), // https://jsonapi.org | ||||
|     TEXT_HTML("text/html"); | ||||
| 
 | ||||
|     private final String value; | ||||
| 
 | ||||
|     MimeType(String value) { | ||||
|         this.value = value; | ||||
|     } | ||||
| 
 | ||||
|     String getValue() { | ||||
|         return value; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,123 @@ | |||
| package com.getcapacitor.util; | ||||
| 
 | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collections; | ||||
| import java.util.List; | ||||
| 
 | ||||
| public interface HostMask { | ||||
|     boolean matches(String host); | ||||
| 
 | ||||
|     class Parser { | ||||
| 
 | ||||
|         private static HostMask NOTHING = new Nothing(); | ||||
| 
 | ||||
|         public static HostMask parse(String[] masks) { | ||||
|             return masks == null ? NOTHING : HostMask.Any.parse(masks); | ||||
|         } | ||||
| 
 | ||||
|         public static HostMask parse(String mask) { | ||||
|             return mask == null ? NOTHING : HostMask.Simple.parse(mask); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Simple implements HostMask { | ||||
| 
 | ||||
|         private final List<String> maskParts; | ||||
| 
 | ||||
|         private Simple(List<String> maskParts) { | ||||
|             if (maskParts == null) { | ||||
|                 throw new IllegalArgumentException("Mask parts can not be null"); | ||||
|             } | ||||
|             this.maskParts = maskParts; | ||||
|         } | ||||
| 
 | ||||
|         static Simple parse(String mask) { | ||||
|             List<String> parts = Util.splitAndReverse(mask); | ||||
|             return new Simple(parts); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public boolean matches(String host) { | ||||
|             if (host == null) { | ||||
|                 return false; | ||||
|             } | ||||
|             List<String> hostParts = Util.splitAndReverse(host); | ||||
|             int hostSize = hostParts.size(); | ||||
|             int maskSize = maskParts.size(); | ||||
|             if (maskSize > 1 && hostSize != maskSize) { | ||||
|                 return false; | ||||
|             } | ||||
| 
 | ||||
|             int minSize = Math.min(hostSize, maskSize); | ||||
| 
 | ||||
|             for (int i = 0; i < minSize; i++) { | ||||
|                 String maskPart = maskParts.get(i); | ||||
|                 String hostPart = hostParts.get(i); | ||||
|                 if (!Util.matches(maskPart, hostPart)) { | ||||
|                     return false; | ||||
|                 } | ||||
|             } | ||||
|             return true; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Any implements HostMask { | ||||
| 
 | ||||
|         private final List<? extends HostMask> masks; | ||||
| 
 | ||||
|         Any(List<? extends HostMask> masks) { | ||||
|             this.masks = masks; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public boolean matches(String host) { | ||||
|             for (HostMask mask : masks) { | ||||
|                 if (mask.matches(host)) { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         static Any parse(String... rawMasks) { | ||||
|             List<HostMask.Simple> masks = new ArrayList<>(); | ||||
|             for (String raw : rawMasks) { | ||||
|                 masks.add(HostMask.Simple.parse(raw)); | ||||
|             } | ||||
|             return new Any(masks); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Nothing implements HostMask { | ||||
| 
 | ||||
|         @Override | ||||
|         public boolean matches(String host) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     class Util { | ||||
| 
 | ||||
|         static boolean matches(String mask, String string) { | ||||
|             if (mask == null) { | ||||
|                 return false; | ||||
|             } else if ("*".equals(mask)) { | ||||
|                 return true; | ||||
|             } else if (string == null) { | ||||
|                 return false; | ||||
|             } else { | ||||
|                 return mask.toUpperCase().equals(string.toUpperCase()); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         static List<String> splitAndReverse(String string) { | ||||
|             if (string == null) { | ||||
|                 throw new IllegalArgumentException("Can not split null argument"); | ||||
|             } | ||||
|             List<String> parts = Arrays.asList(string.split("\\.")); | ||||
|             Collections.reverse(parts); | ||||
|             return parts; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,27 @@ | |||
| package com.getcapacitor.util; | ||||
| 
 | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import android.os.Build; | ||||
| 
 | ||||
| public class InternalUtils { | ||||
| 
 | ||||
|     public static PackageInfo getPackageInfo(PackageManager pm, String packageName) throws PackageManager.NameNotFoundException { | ||||
|         return InternalUtils.getPackageInfo(pm, packageName, 0); | ||||
|     } | ||||
| 
 | ||||
|     public static PackageInfo getPackageInfo(PackageManager pm, String packageName, long flags) | ||||
|         throws PackageManager.NameNotFoundException { | ||||
|         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { | ||||
|             return pm.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(flags)); | ||||
|         } else { | ||||
|             return getPackageInfoLegacy(pm, packageName, (int) flags); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @SuppressWarnings("deprecation") | ||||
|     private static PackageInfo getPackageInfoLegacy(PackageManager pm, String packageName, long flags) | ||||
|         throws PackageManager.NameNotFoundException { | ||||
|         return pm.getPackageInfo(packageName, (int) flags); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,166 @@ | |||
| package com.getcapacitor.util; | ||||
| 
 | ||||
| import org.json.JSONArray; | ||||
| import org.json.JSONException; | ||||
| import org.json.JSONObject; | ||||
| 
 | ||||
| /** | ||||
|  * Helper methods for parsing JSON objects. | ||||
|  */ | ||||
| public class JSONUtils { | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string value from the given JSON object. | ||||
|      * | ||||
|      * @param jsonObject A JSON object to search | ||||
|      * @param key A key to fetch from the JSON object | ||||
|      * @param defaultValue A default value to return if the key cannot be found | ||||
|      * @return The value at the given key in the JSON object, or the default value | ||||
|      */ | ||||
|     public static String getString(JSONObject jsonObject, String key, String defaultValue) { | ||||
|         String k = getDeepestKey(key); | ||||
|         try { | ||||
|             JSONObject o = getDeepestObject(jsonObject, key); | ||||
| 
 | ||||
|             String value = o.getString(k); | ||||
|             if (value == null) { | ||||
|                 return defaultValue; | ||||
|             } | ||||
|             return value; | ||||
|         } catch (JSONException ignore) { | ||||
|             // value was not found | ||||
|         } | ||||
| 
 | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a boolean value from the given JSON object. | ||||
|      * | ||||
|      * @param jsonObject A JSON object to search | ||||
|      * @param key A key to fetch from the JSON object | ||||
|      * @param defaultValue A default value to return if the key cannot be found | ||||
|      * @return The value at the given key in the JSON object, or the default value | ||||
|      */ | ||||
|     public static boolean getBoolean(JSONObject jsonObject, String key, boolean defaultValue) { | ||||
|         String k = getDeepestKey(key); | ||||
|         try { | ||||
|             JSONObject o = getDeepestObject(jsonObject, key); | ||||
| 
 | ||||
|             return o.getBoolean(k); | ||||
|         } catch (JSONException ignore) { | ||||
|             // value was not found | ||||
|         } | ||||
| 
 | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get an int value from the given JSON object. | ||||
|      * | ||||
|      * @param jsonObject A JSON object to search | ||||
|      * @param key A key to fetch from the JSON object | ||||
|      * @param defaultValue A default value to return if the key cannot be found | ||||
|      * @return The value at the given key in the JSON object, or the default value | ||||
|      */ | ||||
|     public static int getInt(JSONObject jsonObject, String key, int defaultValue) { | ||||
|         String k = getDeepestKey(key); | ||||
|         try { | ||||
|             JSONObject o = getDeepestObject(jsonObject, key); | ||||
|             return o.getInt(k); | ||||
|         } catch (JSONException ignore) { | ||||
|             // value was not found | ||||
|         } | ||||
| 
 | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a JSON object value from the given JSON object. | ||||
|      * | ||||
|      * @param jsonObject A JSON object to search | ||||
|      * @param key A key to fetch from the JSON object | ||||
|      * @return The value from the config, if exists. Null if not | ||||
|      */ | ||||
|     public static JSONObject getObject(JSONObject jsonObject, String key) { | ||||
|         String k = getDeepestKey(key); | ||||
|         try { | ||||
|             JSONObject o = getDeepestObject(jsonObject, key); | ||||
| 
 | ||||
|             return o.getJSONObject(k); | ||||
|         } catch (JSONException ignore) { | ||||
|             // value was not found | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get a string array value from the given JSON object. | ||||
|      * | ||||
|      * @param jsonObject A JSON object to search | ||||
|      * @param key A key to fetch from the JSON object | ||||
|      * @param defaultValue A default value to return if the key cannot be found | ||||
|      * @return The value at the given key in the JSON object, or the default value | ||||
|      */ | ||||
|     public static String[] getArray(JSONObject jsonObject, String key, String[] defaultValue) { | ||||
|         String k = getDeepestKey(key); | ||||
|         try { | ||||
|             JSONObject o = getDeepestObject(jsonObject, key); | ||||
| 
 | ||||
|             JSONArray a = o.getJSONArray(k); | ||||
|             if (a == null) { | ||||
|                 return defaultValue; | ||||
|             } | ||||
| 
 | ||||
|             int l = a.length(); | ||||
|             String[] value = new String[l]; | ||||
| 
 | ||||
|             for (int i = 0; i < l; i++) { | ||||
|                 value[i] = (String) a.get(i); | ||||
|             } | ||||
| 
 | ||||
|             return value; | ||||
|         } catch (JSONException ignore) { | ||||
|             // value was not found | ||||
|         } | ||||
| 
 | ||||
|         return defaultValue; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a JSON key path, gets the deepest key. | ||||
|      * | ||||
|      * @param key The key path | ||||
|      * @return The deepest key | ||||
|      */ | ||||
|     private static String getDeepestKey(String key) { | ||||
|         String[] parts = key.split("\\."); | ||||
|         if (parts.length > 0) { | ||||
|             return parts[parts.length - 1]; | ||||
|         } | ||||
| 
 | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a JSON object and key path, gets the deepest object in the path. | ||||
|      * | ||||
|      * @param jsonObject A JSON object | ||||
|      * @param key The key path to follow | ||||
|      * @return The deepest object along the key path | ||||
|      * @throws JSONException Thrown if any JSON errors | ||||
|      */ | ||||
|     private static JSONObject getDeepestObject(JSONObject jsonObject, String key) throws JSONException { | ||||
|         String[] parts = key.split("\\."); | ||||
|         JSONObject o = jsonObject; | ||||
| 
 | ||||
|         // Search until the second to last part of the key | ||||
|         for (int i = 0; i < parts.length - 1; i++) { | ||||
|             String k = parts[i]; | ||||
|             o = o.getJSONObject(k); | ||||
|         } | ||||
| 
 | ||||
|         return o; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,114 @@ | |||
| package com.getcapacitor.util; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.content.pm.PackageInfo; | ||||
| import android.content.pm.PackageManager; | ||||
| import androidx.core.app.ActivityCompat; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| 
 | ||||
| /** | ||||
|  * A helper class for checking permissions. | ||||
|  * | ||||
|  * @since 3.0.0 | ||||
|  */ | ||||
| public class PermissionHelper { | ||||
| 
 | ||||
|     /** | ||||
|      * Checks if a list of given permissions are all granted by the user | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      * @param permissions Permissions to check. | ||||
|      * @return True if all permissions are granted, false if at least one is not. | ||||
|      */ | ||||
|     public static boolean hasPermissions(Context context, String[] permissions) { | ||||
|         for (String perm : permissions) { | ||||
|             if (ActivityCompat.checkSelfPermission(context, perm) != PackageManager.PERMISSION_GRANTED) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether the given permission has been defined in the AndroidManifest.xml | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      * @param permission A permission to check. | ||||
|      * @return True if the permission has been defined in the Manifest, false if not. | ||||
|      */ | ||||
|     public static boolean hasDefinedPermission(Context context, String permission) { | ||||
|         boolean hasPermission = false; | ||||
|         String[] requestedPermissions = PermissionHelper.getManifestPermissions(context); | ||||
|         if (requestedPermissions != null && requestedPermissions.length > 0) { | ||||
|             List<String> requestedPermissionsList = Arrays.asList(requestedPermissions); | ||||
|             ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); | ||||
|             if (requestedPermissionsArrayList.contains(permission)) { | ||||
|                 hasPermission = true; | ||||
|             } | ||||
|         } | ||||
|         return hasPermission; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check whether all of the given permissions have been defined in the AndroidManifest.xml | ||||
|      * @param context the app context | ||||
|      * @param permissions a list of permissions | ||||
|      * @return true only if all permissions are defined in the AndroidManifest.xml | ||||
|      */ | ||||
|     public static boolean hasDefinedPermissions(Context context, String[] permissions) { | ||||
|         for (String permission : permissions) { | ||||
|             if (!PermissionHelper.hasDefinedPermission(context, permission)) { | ||||
|                 return false; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the permissions defined in AndroidManifest.xml | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      * @return The permissions defined in AndroidManifest.xml | ||||
|      */ | ||||
|     public static String[] getManifestPermissions(Context context) { | ||||
|         String[] requestedPermissions = null; | ||||
|         try { | ||||
|             PackageManager pm = context.getPackageManager(); | ||||
|             PackageInfo packageInfo = InternalUtils.getPackageInfo(pm, context.getPackageName(), PackageManager.GET_PERMISSIONS); | ||||
| 
 | ||||
|             if (packageInfo != null) { | ||||
|                 requestedPermissions = packageInfo.requestedPermissions; | ||||
|             } | ||||
|         } catch (Exception ex) {} | ||||
|         return requestedPermissions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Given a list of permissions, return a new list with the ones not present in AndroidManifest.xml | ||||
|      * | ||||
|      * @since 3.0.0 | ||||
|      * @param neededPermissions The permissions needed. | ||||
|      * @return The permissions not present in AndroidManifest.xml | ||||
|      */ | ||||
|     public static String[] getUndefinedPermissions(Context context, String[] neededPermissions) { | ||||
|         ArrayList<String> undefinedPermissions = new ArrayList<>(); | ||||
|         String[] requestedPermissions = getManifestPermissions(context); | ||||
|         if (requestedPermissions != null && requestedPermissions.length > 0) { | ||||
|             List<String> requestedPermissionsList = Arrays.asList(requestedPermissions); | ||||
|             ArrayList<String> requestedPermissionsArrayList = new ArrayList<>(requestedPermissionsList); | ||||
|             for (String permission : neededPermissions) { | ||||
|                 if (!requestedPermissionsArrayList.contains(permission)) { | ||||
|                     undefinedPermissions.add(permission); | ||||
|                 } | ||||
|             } | ||||
|             String[] undefinedPermissionArray = new String[undefinedPermissions.size()]; | ||||
|             undefinedPermissionArray = undefinedPermissions.toArray(undefinedPermissionArray); | ||||
| 
 | ||||
|             return undefinedPermissionArray; | ||||
|         } | ||||
|         return neededPermissions; | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,28 @@ | |||
| package com.getcapacitor.util; | ||||
| 
 | ||||
| import android.graphics.Color; | ||||
| 
 | ||||
| public class WebColor { | ||||
| 
 | ||||
|     /** | ||||
|      * Parse the color string, and return the corresponding color-int. If the string cannot be parsed, throws an IllegalArgumentException exception. | ||||
|      * @param colorString The hexadecimal color string. The format is an RGB or RGBA hex string. | ||||
|      * @return The corresponding color as an int. | ||||
|      */ | ||||
|     public static int parseColor(String colorString) { | ||||
|         String formattedColor = colorString; | ||||
|         if (colorString.charAt(0) != '#') { | ||||
|             formattedColor = "#" + formattedColor; | ||||
|         } | ||||
| 
 | ||||
|         if (formattedColor.length() != 7 && formattedColor.length() != 9) { | ||||
|             throw new IllegalArgumentException("The encoded color space is invalid or unknown"); | ||||
|         } else if (formattedColor.length() == 7) { | ||||
|             return Color.parseColor(formattedColor); | ||||
|         } else { | ||||
|             // Convert to Android format #AARRGGBB from #RRGGBBAA | ||||
|             formattedColor = "#" + formattedColor.substring(7) + formattedColor.substring(1, 7); | ||||
|             return Color.parseColor(formattedColor); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,15 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:app="http://schemas.android.com/apk/res-auto" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     tools:context="com.getcapacitor.BridgeActivity" | ||||
|     > | ||||
| 
 | ||||
|     <com.getcapacitor.CapacitorWebView | ||||
|         android:id="@+id/webview" | ||||
|         android:layout_width="fill_parent" | ||||
|         android:layout_height="fill_parent" /> | ||||
| 
 | ||||
| </androidx.coordinatorlayout.widget.CoordinatorLayout> | ||||
|  | @ -0,0 +1,13 @@ | |||
| <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent" | ||||
|     android:background="#F0FF1414" | ||||
|     tools:context="com.getcapacitor.BridgeFragment"> | ||||
| 
 | ||||
|   <com.getcapacitor.CapacitorWebView | ||||
|       android:id="@+id/webview" | ||||
|       android:layout_width="fill_parent" | ||||
|       android:layout_height="fill_parent" /> | ||||
| 
 | ||||
| </FrameLayout> | ||||
|  | @ -0,0 +1,14 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <TextView | ||||
|         android:id="@+id/textView" | ||||
|         android:layout_width="0dp" | ||||
|         android:layout_height="match_parent" | ||||
|         android:layout_weight="1" | ||||
|         android:text="@string/no_webview_text" | ||||
|         android:gravity="center" | ||||
|         android:textSize="48sp" /> | ||||
| </LinearLayout> | ||||
|  | @ -0,0 +1,6 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
|   <declare-styleable name="bridge_fragment"> | ||||
|     <attr name="start_dir" format="string"/> | ||||
|   </declare-styleable> | ||||
| </resources> | ||||
|  | @ -0,0 +1,6 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources xmlns:tools="http://schemas.android.com/tools"> | ||||
|     <color tools:ignore="UnusedResources" name="colorPrimary">#3F51B5</color> | ||||
|     <color tools:ignore="UnusedResources" name="colorPrimaryDark">#303F9F</color> | ||||
|     <color tools:ignore="UnusedResources" name="colorAccent">#FF4081</color> | ||||
| </resources> | ||||
|  | @ -0,0 +1,3 @@ | |||
| <resources> | ||||
|     <string name="no_webview_text">This app requires a WebView to work</string> | ||||
| </resources> | ||||
|  | @ -0,0 +1,6 @@ | |||
| <resources> | ||||
|     <style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar"> | ||||
|         <item name="windowActionBar">false</item> | ||||
|         <item name="windowNoTitle">true</item> | ||||
|     </style> | ||||
| </resources> | ||||
							
								
								
									
										31
									
								
								@capacitor/android/package.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								@capacitor/android/package.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,31 @@ | |||
| { | ||||
|   "name": "@capacitor/android", | ||||
|   "version": "6.1.2", | ||||
|   "description": "Capacitor: Cross-platform apps with JavaScript and the web", | ||||
|   "homepage": "https://capacitorjs.com", | ||||
|   "author": "Ionic Team <hi@ionic.io> (https://ionic.io)", | ||||
|   "license": "MIT", | ||||
|   "repository": { | ||||
|     "type": "git", | ||||
|     "url": "git+https://github.com/ionic-team/capacitor.git" | ||||
|   }, | ||||
|   "bugs": { | ||||
|     "url": "https://github.com/ionic-team/capacitor/issues" | ||||
|   }, | ||||
|   "files": [ | ||||
|     "capacitor/build.gradle", | ||||
|     "capacitor/lint-baseline.xml", | ||||
|     "capacitor/lint.xml", | ||||
|     "capacitor/proguard-rules.pro", | ||||
|     "capacitor/src/main/" | ||||
|   ], | ||||
|   "scripts": { | ||||
|     "verify": "./gradlew clean lint build test -b capacitor/build.gradle" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "@capacitor/core": "^6.1.0" | ||||
|   }, | ||||
|   "publishConfig": { | ||||
|     "access": "public" | ||||
|   } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue