diff --git a/@capacitor/android/capacitor/build.gradle b/@capacitor/android/capacitor/build.gradle index 5e2a4011..d50aa275 100644 --- a/@capacitor/android/capacitor/build.gradle +++ b/@capacitor/android/capacitor/build.gradle @@ -33,6 +33,7 @@ buildscript { tasks.withType(Javadoc).all { enabled = false } apply plugin: 'com.android.library' +apply plugin: 'org.jetbrains.kotlin.android' if (System.getenv("CAP_PUBLISH") == "true") { apply plugin: 'io.github.gradle-nexus.publish-plugin' @@ -70,6 +71,9 @@ android { publishing { singleVariant("release") } + kotlinOptions { + jvmTarget = '17' + } } repositories { @@ -86,6 +90,7 @@ dependencies { implementation "androidx.fragment:fragment:$androidxFragmentVersion" implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" implementation "androidx.webkit:webkit:$androidxWebkitVersion" + implementation 'androidx.core:core-ktx:1.13.1' testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.java deleted file mode 100644 index 1b01134c..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.java +++ /dev/null @@ -1,1568 +0,0 @@ -package com.getcapacitor; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.pm.ApplicationInfo; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.net.Uri; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.HandlerThread; -import android.webkit.ValueCallback; -import android.webkit.WebSettings; -import android.webkit.WebView; -import androidx.activity.result.ActivityResultCallback; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContract; -import androidx.annotation.NonNull; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import androidx.core.content.pm.PackageInfoCompat; -import androidx.fragment.app.Fragment; -import com.getcapacitor.android.R; -import com.getcapacitor.annotation.CapacitorPlugin; -import com.getcapacitor.annotation.Permission; -import com.getcapacitor.cordova.MockCordovaInterfaceImpl; -import com.getcapacitor.cordova.MockCordovaWebViewImpl; -import com.getcapacitor.util.HostMask; -import com.getcapacitor.util.InternalUtils; -import com.getcapacitor.util.PermissionHelper; -import com.getcapacitor.util.WebColor; -import java.io.File; -import java.net.SocketTimeoutException; -import java.net.URL; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import org.apache.cordova.ConfigXmlParser; -import org.apache.cordova.CordovaPreferences; -import org.apache.cordova.CordovaWebView; -import org.apache.cordova.PluginEntry; -import org.apache.cordova.PluginManager; -import org.json.JSONException; - -/** - * The Bridge class is the main engine of Capacitor. It manages - * loading and communicating with all Plugins, - * proxying Native events to Plugins, executing Plugin methods, - * communicating with the WebView, and a whole lot more. - * - * Generally, you'll not use Bridge directly, instead, extend from BridgeActivity - * to get a WebView instance and proxy native events automatically. - * - * If you want to use this Bridge in an existing Android app, please - * see the source for BridgeActivity for the methods you'll need to - * pass through to Bridge: - * - * BridgeActivity.java - */ -public class Bridge { - - private static final String PREFS_NAME = "CapacitorSettings"; - private static final String PERMISSION_PREFS_NAME = "PluginPermStates"; - private static final String BUNDLE_LAST_PLUGIN_ID_KEY = "capacitorLastActivityPluginId"; - private static final String BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY = "capacitorLastActivityPluginMethod"; - private static final String BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY = "capacitorLastPluginCallOptions"; - private static final String BUNDLE_PLUGIN_CALL_BUNDLE_KEY = "capacitorLastPluginCallBundle"; - private static final String LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode"; - private static final String LAST_BINARY_VERSION_NAME = "lastBinaryVersionName"; - private static final String MINIMUM_ANDROID_WEBVIEW_ERROR = "System WebView is not supported"; - - // The name of the directory we use to look for index.html and the rest of our web assets - public static final String DEFAULT_WEB_ASSET_DIR = "public"; - public static final String CAPACITOR_HTTP_SCHEME = "http"; - public static final String CAPACITOR_HTTPS_SCHEME = "https"; - public static final String CAPACITOR_FILE_START = "/_capacitor_file_"; - public static final String CAPACITOR_CONTENT_START = "/_capacitor_content_"; - public static final String CAPACITOR_HTTP_INTERCEPTOR_START = "/_capacitor_http_interceptor_"; - public static final String CAPACITOR_HTTPS_INTERCEPTOR_START = "/_capacitor_https_interceptor_"; - - public static final int DEFAULT_ANDROID_WEBVIEW_VERSION = 60; - public static final int MINIMUM_ANDROID_WEBVIEW_VERSION = 55; - public static final int DEFAULT_HUAWEI_WEBVIEW_VERSION = 10; - public static final int MINIMUM_HUAWEI_WEBVIEW_VERSION = 10; - - // Loaded Capacitor config - private CapConfig config; - - // A reference to the main activity for the app - private final AppCompatActivity context; - // A reference to the containing Fragment if used - private final Fragment fragment; - private WebViewLocalServer localServer; - private String localUrl; - private String appUrl; - private String appUrlConfig; - private HostMask appAllowNavigationMask; - private Set allowedOriginRules = new HashSet(); - private ArrayList authorities = new ArrayList<>(); - // A reference to the main WebView for the app - private final WebView webView; - public final MockCordovaInterfaceImpl cordovaInterface; - private CordovaWebView cordovaWebView; - private CordovaPreferences preferences; - private BridgeWebViewClient webViewClient; - private App app; - - // Our MessageHandler for sending and receiving data to the WebView - private final MessageHandler msgHandler; - - // The ThreadHandler for executing plugin calls - private final HandlerThread handlerThread = new HandlerThread("CapacitorPlugins"); - - // Our Handler for posting plugin calls. Created from the ThreadHandler - private Handler taskHandler = null; - - private final List> initialPlugins; - - private final List pluginInstances; - - // A map of Plugin Id's to PluginHandle's - private Map plugins = new HashMap<>(); - - // Stored plugin calls that we're keeping around to call again someday - private Map savedCalls = new HashMap<>(); - - // The call IDs of saved plugin calls with associated plugin id for handling permissions - private Map> savedPermissionCallIds = new HashMap<>(); - - // Store a plugin that started a new activity, in case we need to resume - // the app and return that data back - private PluginCall pluginCallForLastActivity; - - // Any URI that was passed to the app on start - private Uri intentUri; - - // A list of listeners that trigger when webView events occur - private List webViewListeners = new ArrayList<>(); - - // An interface to manipulate route resolving - private RouteProcessor routeProcessor; - - // A pre-determined path to load the bridge - private ServerPath serverPath; - - /** - * Create the Bridge with a reference to the main {@link Activity} for the - * app, and a reference to the {@link WebView} our app will use. - * @param context - * @param webView - * @deprecated Use {@link Bridge.Builder} to create Bridge instances - */ - @Deprecated - public Bridge( - AppCompatActivity context, - WebView webView, - List> initialPlugins, - MockCordovaInterfaceImpl cordovaInterface, - PluginManager pluginManager, - CordovaPreferences preferences, - CapConfig config - ) { - this(context, null, null, webView, initialPlugins, new ArrayList<>(), cordovaInterface, pluginManager, preferences, config); - } - - private Bridge( - AppCompatActivity context, - ServerPath serverPath, - Fragment fragment, - WebView webView, - List> initialPlugins, - List pluginInstances, - MockCordovaInterfaceImpl cordovaInterface, - PluginManager pluginManager, - CordovaPreferences preferences, - CapConfig config - ) { - this.app = new App(); - this.serverPath = serverPath; - this.context = context; - this.fragment = fragment; - this.webView = webView; - this.webViewClient = new BridgeWebViewClient(this); - this.initialPlugins = initialPlugins; - this.pluginInstances = pluginInstances; - this.cordovaInterface = cordovaInterface; - this.preferences = preferences; - - // Start our plugin execution threads and handlers - handlerThread.start(); - taskHandler = new Handler(handlerThread.getLooper()); - - this.config = config != null ? config : CapConfig.loadDefault(getActivity()); - Logger.init(this.config); - - // Initialize web view and message handler for it - this.initWebView(); - this.setAllowedOriginRules(); - this.msgHandler = new MessageHandler(this, webView, pluginManager); - - // Grab any intent info that our app was launched with - Intent intent = context.getIntent(); - this.intentUri = intent.getData(); - // Register our core plugins - this.registerAllPlugins(); - - this.loadWebView(); - } - - private void setAllowedOriginRules() { - String[] appAllowNavigationConfig = this.config.getAllowNavigation(); - String authority = this.getHost(); - String scheme = this.getScheme(); - allowedOriginRules.add(scheme + "://" + authority); - if (this.getServerUrl() != null) { - allowedOriginRules.add(this.getServerUrl()); - } - if (appAllowNavigationConfig != null) { - for (String allowNavigation : appAllowNavigationConfig) { - if (!allowNavigation.startsWith("http")) { - allowedOriginRules.add("https://" + allowNavigation); - } else { - allowedOriginRules.add(allowNavigation); - } - } - authorities.addAll(Arrays.asList(appAllowNavigationConfig)); - } - this.appAllowNavigationMask = HostMask.Parser.parse(appAllowNavigationConfig); - } - - public App getApp() { - return app; - } - - private void loadWebView() { - final boolean html5mode = this.config.isHTML5Mode(); - - // Start the local web server - localServer = new WebViewLocalServer(context, this, getJSInjector(), authorities, html5mode); - localServer.hostAssets(DEFAULT_WEB_ASSET_DIR); - - Logger.debug("Loading app at " + appUrl); - - webView.setWebChromeClient(new BridgeWebChromeClient(this)); - webView.setWebViewClient(this.webViewClient); - - if (!isDeployDisabled() && !isNewBinary()) { - SharedPreferences prefs = getContext() - .getSharedPreferences(com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - String path = prefs.getString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, null); - if (path != null && !path.isEmpty() && new File(path).exists()) { - setServerBasePath(path); - } - } - if (!this.isMinimumWebViewInstalled()) { - String errorUrl = this.getErrorUrl(); - if (errorUrl != null) { - webView.loadUrl(errorUrl); - return; - } else { - Logger.error(MINIMUM_ANDROID_WEBVIEW_ERROR); - } - } - - // If serverPath configured, start server based on provided path - if (serverPath != null) { - if (serverPath.getType() == ServerPath.PathType.ASSET_PATH) { - setServerAssetPath(serverPath.getPath()); - } else { - setServerBasePath(serverPath.getPath()); - } - } else { - // Get to work - webView.loadUrl(appUrl); - } - } - - @SuppressLint("WebViewApiAvailability") - public boolean isMinimumWebViewInstalled() { - PackageManager pm = getContext().getPackageManager(); - - // Check getCurrentWebViewPackage() directly if above Android 8 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - PackageInfo info = WebView.getCurrentWebViewPackage(); - Pattern pattern = Pattern.compile("(\\d+)"); - Matcher matcher = pattern.matcher(info.versionName); - if (matcher.find()) { - String majorVersionStr = matcher.group(0); - int majorVersion = Integer.parseInt(majorVersionStr); - if (info.packageName.equals("com.huawei.webview")) { - return majorVersion >= config.getMinHuaweiWebViewVersion(); - } - return majorVersion >= config.getMinWebViewVersion(); - } else { - return false; - } - } - - // Otherwise manually check WebView versions - try { - String webViewPackage = "com.google.android.webview"; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - webViewPackage = "com.android.chrome"; - } - PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackage); - String majorVersionStr = info.versionName.split("\\.")[0]; - int majorVersion = Integer.parseInt(majorVersionStr); - return majorVersion >= config.getMinWebViewVersion(); - } catch (Exception ex) { - Logger.warn("Unable to get package info for 'com.google.android.webview'" + ex.toString()); - } - - try { - PackageInfo info = InternalUtils.getPackageInfo(pm, "com.android.webview"); - String majorVersionStr = info.versionName.split("\\.")[0]; - int majorVersion = Integer.parseInt(majorVersionStr); - return majorVersion >= config.getMinWebViewVersion(); - } catch (Exception ex) { - Logger.warn("Unable to get package info for 'com.android.webview'" + ex.toString()); - } - - final int amazonFireMajorWebViewVersion = extractWebViewMajorVersion(pm, "com.amazon.webview.chromium"); - if (amazonFireMajorWebViewVersion >= config.getMinWebViewVersion()) { - return true; - } - - // Could not detect any webview, return false - return false; - } - - private int extractWebViewMajorVersion(final PackageManager pm, final String webViewPackageName) { - try { - final PackageInfo info = InternalUtils.getPackageInfo(pm, webViewPackageName); - final String majorVersionStr = info.versionName.split("\\.")[0]; - final int majorVersion = Integer.parseInt(majorVersionStr); - return majorVersion; - } catch (Exception ex) { - Logger.warn(String.format("Unable to get package info for '%s' with err '%s'", webViewPackageName, ex)); - } - return 0; - } - - public boolean launchIntent(Uri url) { - /* - * Give plugins the chance to handle the url - */ - for (Map.Entry entry : plugins.entrySet()) { - Plugin plugin = entry.getValue().getInstance(); - if (plugin != null) { - Boolean shouldOverrideLoad = plugin.shouldOverrideLoad(url); - if (shouldOverrideLoad != null) { - return shouldOverrideLoad; - } - } - } - - if (url.getScheme().equals("data") || url.getScheme().equals("blob")) { - return false; - } - - Uri appUri = Uri.parse(appUrl); - if ( - !(appUri.getHost().equals(url.getHost()) && url.getScheme().equals(appUri.getScheme())) && - !appAllowNavigationMask.matches(url.getHost()) - ) { - try { - Intent openIntent = new Intent(Intent.ACTION_VIEW, url); - getContext().startActivity(openIntent); - } catch (ActivityNotFoundException e) { - // TODO - trigger an event - } - return true; - } - return false; - } - - private boolean isNewBinary() { - String versionCode = ""; - String versionName = ""; - SharedPreferences prefs = getContext() - .getSharedPreferences(com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, Activity.MODE_PRIVATE); - String lastVersionCode = prefs.getString(LAST_BINARY_VERSION_CODE, null); - String lastVersionName = prefs.getString(LAST_BINARY_VERSION_NAME, null); - - try { - PackageManager pm = getContext().getPackageManager(); - PackageInfo pInfo = InternalUtils.getPackageInfo(pm, getContext().getPackageName()); - versionCode = Integer.toString((int) PackageInfoCompat.getLongVersionCode(pInfo)); - versionName = pInfo.versionName; - } catch (Exception ex) { - Logger.error("Unable to get package info", ex); - } - - if (!versionCode.equals(lastVersionCode) || !versionName.equals(lastVersionName)) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(LAST_BINARY_VERSION_CODE, versionCode); - editor.putString(LAST_BINARY_VERSION_NAME, versionName); - editor.putString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, ""); - editor.apply(); - return true; - } - return false; - } - - public boolean isDeployDisabled() { - return preferences.getBoolean("DisableDeploy", false); - } - - public boolean shouldKeepRunning() { - return preferences.getBoolean("KeepRunning", true); - } - - public void handleAppUrlLoadError(Exception ex) { - if (ex instanceof SocketTimeoutException) { - Logger.error( - "Unable to load app. Ensure the server is running at " + - appUrl + - ", or modify the " + - "appUrl setting in capacitor.config.json (make sure to npx cap copy after to commit changes).", - ex - ); - } - } - - public boolean isDevMode() { - return (getActivity().getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0; - } - - protected void setCordovaWebView(CordovaWebView cordovaWebView) { - this.cordovaWebView = cordovaWebView; - } - - /** - * Get the Context for the App - * @return - */ - public Context getContext() { - return this.context; - } - - /** - * Get the activity for the app - * @return - */ - public AppCompatActivity getActivity() { - return this.context; - } - - /** - * Get the fragment for the app, if applicable. This will likely be null unless Capacitor - * is being used embedded in a Native Android app. - * - * @return The fragment containing the Capacitor WebView. - */ - public Fragment getFragment() { - return this.fragment; - } - - /** - * Get the core WebView under Capacitor's control - * @return - */ - public WebView getWebView() { - return this.webView; - } - - /** - * Get the URI that was used to launch the app (if any) - * @return - */ - public Uri getIntentUri() { - return intentUri; - } - - /** - * Get scheme that is used to serve content - * @return - */ - public String getScheme() { - return this.config.getAndroidScheme(); - } - - /** - * Get host name that is used to serve content - * @return - */ - public String getHost() { - return this.config.getHostname(); - } - - /** - * Get the server url that is used to serve content - * @return - */ - public String getServerUrl() { - return this.config.getServerUrl(); - } - - public String getErrorUrl() { - String errorPath = this.config.getErrorPath(); - - if (errorPath != null && !errorPath.trim().isEmpty()) { - String authority = this.getHost(); - String scheme = this.getScheme(); - - String localUrl = scheme + "://" + authority; - - return localUrl + "/" + errorPath; - } - - return null; - } - - public String getAppUrl() { - return appUrl; - } - - public CapConfig getConfig() { - return this.config; - } - - public void reset() { - savedCalls = new HashMap<>(); - } - - /** - * Initialize the WebView, setting required flags - */ - @SuppressLint("SetJavaScriptEnabled") - private void initWebView() { - WebSettings settings = webView.getSettings(); - settings.setJavaScriptEnabled(true); - settings.setDomStorageEnabled(true); - settings.setGeolocationEnabled(true); - settings.setDatabaseEnabled(true); - settings.setMediaPlaybackRequiresUserGesture(false); - settings.setJavaScriptCanOpenWindowsAutomatically(true); - if (this.config.isMixedContentAllowed()) { - settings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW); - } - - String appendUserAgent = this.config.getAppendedUserAgentString(); - if (appendUserAgent != null) { - String defaultUserAgent = settings.getUserAgentString(); - settings.setUserAgentString(defaultUserAgent + " " + appendUserAgent); - } - String overrideUserAgent = this.config.getOverriddenUserAgentString(); - if (overrideUserAgent != null) { - settings.setUserAgentString(overrideUserAgent); - } - - String backgroundColor = this.config.getBackgroundColor(); - try { - if (backgroundColor != null) { - webView.setBackgroundColor(WebColor.parseColor(backgroundColor)); - } - } catch (IllegalArgumentException ex) { - Logger.debug("WebView background color not applied"); - } - - if (config.isInitialFocus()) { - webView.requestFocusFromTouch(); - } - - WebView.setWebContentsDebuggingEnabled(this.config.isWebContentsDebuggingEnabled()); - - appUrlConfig = this.getServerUrl(); - String authority = this.getHost(); - authorities.add(authority); - String scheme = this.getScheme(); - - localUrl = scheme + "://" + authority; - - if (appUrlConfig != null) { - try { - URL appUrlObject = new URL(appUrlConfig); - authorities.add(appUrlObject.getAuthority()); - } catch (Exception ex) { - Logger.error("Provided server url is invalid: " + ex.getMessage()); - return; - } - localUrl = appUrlConfig; - appUrl = appUrlConfig; - } else { - appUrl = localUrl; - // custom URL schemes requires path ending with / - if (!scheme.equals(Bridge.CAPACITOR_HTTP_SCHEME) && !scheme.equals(CAPACITOR_HTTPS_SCHEME)) { - appUrl += "/"; - } - } - - String appUrlPath = this.config.getStartPath(); - if (appUrlPath != null && !appUrlPath.trim().isEmpty()) { - appUrl += appUrlPath; - } - } - - /** - * Register our core Plugin APIs - */ - private void registerAllPlugins() { - this.registerPlugin(com.getcapacitor.plugin.CapacitorCookies.class); - this.registerPlugin(com.getcapacitor.plugin.WebView.class); - this.registerPlugin(com.getcapacitor.plugin.CapacitorHttp.class); - - for (Class pluginClass : this.initialPlugins) { - this.registerPlugin(pluginClass); - } - - for (Plugin plugin : pluginInstances) { - registerPluginInstance(plugin); - } - } - - /** - * Register additional plugins - * @param pluginClasses the plugins to register - */ - public void registerPlugins(Class[] pluginClasses) { - for (Class plugin : pluginClasses) { - this.registerPlugin(plugin); - } - } - - public void registerPluginInstances(Plugin[] pluginInstances) { - for (Plugin plugin : pluginInstances) { - this.registerPluginInstance(plugin); - } - } - - @SuppressWarnings("deprecation") - private String getLegacyPluginName(Class pluginClass) { - NativePlugin legacyPluginAnnotation = pluginClass.getAnnotation(NativePlugin.class); - if (legacyPluginAnnotation == null) { - Logger.error("Plugin doesn't have the @CapacitorPlugin annotation. Please add it"); - return null; - } - - return legacyPluginAnnotation.name(); - } - - /** - * Register a plugin class - * @param pluginClass a class inheriting from Plugin - */ - public void registerPlugin(Class pluginClass) { - String pluginId = pluginId(pluginClass); - if (pluginId == null) return; - - try { - this.plugins.put(pluginId, new PluginHandle(this, pluginClass)); - } catch (InvalidPluginException ex) { - logInvalidPluginException(pluginClass); - } catch (PluginLoadException ex) { - logPluginLoadException(pluginClass, ex); - } - } - - public void registerPluginInstance(Plugin plugin) { - Class clazz = plugin.getClass(); - String pluginId = pluginId(clazz); - if (pluginId == null) return; - - try { - this.plugins.put(pluginId, new PluginHandle(this, plugin)); - } catch (InvalidPluginException ex) { - logInvalidPluginException(clazz); - } - } - - private String pluginId(Class clazz) { - String pluginName = pluginName(clazz); - String pluginId = clazz.getSimpleName(); - if (pluginName == null) return null; - - if (!pluginName.equals("")) { - pluginId = pluginName; - } - Logger.debug("Registering plugin instance: " + pluginId); - return pluginId; - } - - private String pluginName(Class clazz) { - String pluginName; - CapacitorPlugin pluginAnnotation = clazz.getAnnotation(CapacitorPlugin.class); - if (pluginAnnotation == null) { - pluginName = this.getLegacyPluginName(clazz); - } else { - pluginName = pluginAnnotation.name(); - } - - return pluginName; - } - - private void logInvalidPluginException(Class clazz) { - Logger.error( - "NativePlugin " + - clazz.getName() + - " is invalid. Ensure the @CapacitorPlugin annotation exists on the plugin class and" + - " the class extends Plugin" - ); - } - - private void logPluginLoadException(Class clazz, Exception ex) { - Logger.error("NativePlugin " + clazz.getName() + " failed to load", ex); - } - - public PluginHandle getPlugin(String pluginId) { - return this.plugins.get(pluginId); - } - - /** - * Find the plugin handle that responds to the given request code. This will - * fire after certain Android OS intent results/permission checks/etc. - * @param requestCode - * @return - */ - @Deprecated - @SuppressWarnings("deprecation") - public PluginHandle getPluginWithRequestCode(int requestCode) { - for (PluginHandle handle : this.plugins.values()) { - int[] requestCodes; - - CapacitorPlugin pluginAnnotation = handle.getPluginAnnotation(); - if (pluginAnnotation == null) { - // Check for legacy plugin annotation, @NativePlugin - NativePlugin legacyPluginAnnotation = handle.getLegacyPluginAnnotation(); - if (legacyPluginAnnotation == null) { - continue; - } - - if (legacyPluginAnnotation.permissionRequestCode() == requestCode) { - return handle; - } - - requestCodes = legacyPluginAnnotation.requestCodes(); - - for (int rc : requestCodes) { - if (rc == requestCode) { - return handle; - } - } - } else { - requestCodes = pluginAnnotation.requestCodes(); - - for (int rc : requestCodes) { - if (rc == requestCode) { - return handle; - } - } - } - } - return null; - } - - /** - * Call a method on a plugin. - * @param pluginId the plugin id to use to lookup the plugin handle - * @param methodName the name of the method to call - * @param call the call object to pass to the method - */ - public void callPluginMethod(String pluginId, final String methodName, final PluginCall call) { - try { - final PluginHandle plugin = this.getPlugin(pluginId); - - if (plugin == null) { - Logger.error("unable to find plugin : " + pluginId); - call.errorCallback("unable to find plugin : " + pluginId); - return; - } - - if (Logger.shouldLog()) { - Logger.verbose( - "callback: " + - call.getCallbackId() + - ", pluginId: " + - plugin.getId() + - ", methodName: " + - methodName + - ", methodData: " + - call.getData().toString() - ); - } - - Runnable currentThreadTask = () -> { - try { - plugin.invoke(methodName, call); - - if (call.isKeptAlive()) { - saveCall(call); - } - } catch (PluginLoadException | InvalidPluginMethodException ex) { - Logger.error("Unable to execute plugin method", ex); - } catch (Exception ex) { - Logger.error("Serious error executing plugin", ex); - throw new RuntimeException(ex); - } - }; - - taskHandler.post(currentThreadTask); - } catch (Exception ex) { - Logger.error(Logger.tags("callPluginMethod"), "error : " + ex, null); - call.errorCallback(ex.toString()); - } - } - - /** - * Evaluate JavaScript in the web view. This method - * executes on the main thread automatically. - * @param js the JS to execute - * @param callback an optional ValueCallback that will synchronously receive a value - * after calling the JS - */ - public void eval(final String js, final ValueCallback callback) { - Handler mainHandler = new Handler(context.getMainLooper()); - mainHandler.post(() -> webView.evaluateJavascript(js, callback)); - } - - public void logToJs(final String message, final String level) { - eval("window.Capacitor.logJs(\"" + message + "\", \"" + level + "\")", null); - } - - public void logToJs(final String message) { - logToJs(message, "log"); - } - - public void triggerJSEvent(final String eventName, final String target) { - eval("window.Capacitor.triggerEvent(\"" + eventName + "\", \"" + target + "\")", s -> {}); - } - - public void triggerJSEvent(final String eventName, final String target, final String data) { - eval("window.Capacitor.triggerEvent(\"" + eventName + "\", \"" + target + "\", " + data + ")", s -> {}); - } - - public void triggerWindowJSEvent(final String eventName) { - this.triggerJSEvent(eventName, "window"); - } - - public void triggerWindowJSEvent(final String eventName, final String data) { - this.triggerJSEvent(eventName, "window", data); - } - - public void triggerDocumentJSEvent(final String eventName) { - this.triggerJSEvent(eventName, "document"); - } - - public void triggerDocumentJSEvent(final String eventName, final String data) { - this.triggerJSEvent(eventName, "document", data); - } - - public void execute(Runnable runnable) { - taskHandler.post(runnable); - } - - public void executeOnMainThread(Runnable runnable) { - Handler mainHandler = new Handler(context.getMainLooper()); - - mainHandler.post(runnable); - } - - /** - * Retain a call between plugin invocations - * @param call - */ - public void saveCall(PluginCall call) { - this.savedCalls.put(call.getCallbackId(), call); - } - - /** - * Get a retained plugin call - * @param callbackId the callbackId to use to lookup the call with - * @return the stored call - */ - public PluginCall getSavedCall(String callbackId) { - if (callbackId == null) { - return null; - } - - return this.savedCalls.get(callbackId); - } - - PluginCall getPluginCallForLastActivity() { - PluginCall pluginCallForLastActivity = this.pluginCallForLastActivity; - this.pluginCallForLastActivity = null; - return pluginCallForLastActivity; - } - - void setPluginCallForLastActivity(PluginCall pluginCallForLastActivity) { - this.pluginCallForLastActivity = pluginCallForLastActivity; - } - - /** - * Release a retained call - * @param call a call to release - */ - public void releaseCall(PluginCall call) { - releaseCall(call.getCallbackId()); - } - - /** - * Release a retained call by its ID - * @param callbackId an ID of a callback to release - */ - public void releaseCall(String callbackId) { - this.savedCalls.remove(callbackId); - } - - /** - * Removes the earliest saved call prior to a permissions request for a given plugin and - * returns it. - * - * @return The saved plugin call - */ - protected PluginCall getPermissionCall(String pluginId) { - LinkedList permissionCallIds = this.savedPermissionCallIds.get(pluginId); - String savedCallId = null; - if (permissionCallIds != null) { - savedCallId = permissionCallIds.poll(); - } - - return getSavedCall(savedCallId); - } - - /** - * Save a call to be retrieved after requesting permissions. Calls are saved in order. - * - * @param call The plugin call to save. - */ - protected void savePermissionCall(PluginCall call) { - if (call != null) { - if (!savedPermissionCallIds.containsKey(call.getPluginId())) { - savedPermissionCallIds.put(call.getPluginId(), new LinkedList<>()); - } - - savedPermissionCallIds.get(call.getPluginId()).add(call.getCallbackId()); - saveCall(call); - } - } - - /** - * Register an Activity Result Launcher to the containing Fragment or Activity. - * - * @param contract A contract specifying that an activity can be called with an input of - * type I and produce an output of type O. - * @param callback The callback run on Activity Result. - * @return A registered Activity Result Launcher. - */ - public ActivityResultLauncher registerForActivityResult( - @NonNull final ActivityResultContract contract, - @NonNull final ActivityResultCallback callback - ) { - if (fragment != null) { - return fragment.registerForActivityResult(contract, callback); - } else { - return context.registerForActivityResult(contract, callback); - } - } - - /** - * Build the JSInjector that will be used to inject JS into files served to the app, - * to ensure that Capacitor's JS and the JS for all the plugins is loaded each time. - */ - private JSInjector getJSInjector() { - try { - String globalJS = JSExport.getGlobalJS(context, config.isLoggingEnabled(), isDevMode()); - String bridgeJS = JSExport.getBridgeJS(context); - String pluginJS = JSExport.getPluginJS(plugins.values()); - String cordovaJS = JSExport.getCordovaJS(context); - String cordovaPluginsJS = JSExport.getCordovaPluginJS(context); - String cordovaPluginsFileJS = JSExport.getCordovaPluginsFileJS(context); - String localUrlJS = "window.WEBVIEW_SERVER_URL = '" + localUrl + "';"; - - return new JSInjector(globalJS, bridgeJS, pluginJS, cordovaJS, cordovaPluginsJS, cordovaPluginsFileJS, localUrlJS); - } catch (Exception ex) { - Logger.error("Unable to export Capacitor JS. App will not function!", ex); - } - return null; - } - - /** - * Restore any saved bundle state data - * @param savedInstanceState - */ - public void restoreInstanceState(Bundle savedInstanceState) { - String lastPluginId = savedInstanceState.getString(BUNDLE_LAST_PLUGIN_ID_KEY); - String lastPluginCallMethod = savedInstanceState.getString(BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY); - String lastOptionsJson = savedInstanceState.getString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY); - - if (lastPluginId != null) { - // If we have JSON blob saved, create a new plugin call with the original options - if (lastOptionsJson != null) { - try { - JSObject options = new JSObject(lastOptionsJson); - - pluginCallForLastActivity = - new PluginCall(msgHandler, lastPluginId, PluginCall.CALLBACK_ID_DANGLING, lastPluginCallMethod, options); - } catch (JSONException ex) { - Logger.error("Unable to restore plugin call, unable to parse persisted JSON object", ex); - } - } - - // Let the plugin restore any state it needs - Bundle bundleData = savedInstanceState.getBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY); - PluginHandle lastPlugin = getPlugin(lastPluginId); - if (bundleData != null && lastPlugin != null) { - lastPlugin.getInstance().restoreState(bundleData); - } else { - Logger.error("Unable to restore last plugin call"); - } - } - } - - public void saveInstanceState(Bundle outState) { - Logger.debug("Saving instance state!"); - - // If there was a last PluginCall for a started activity, we need to - // persist it so we can load it again in case our app gets terminated - if (pluginCallForLastActivity != null) { - PluginCall call = pluginCallForLastActivity; - PluginHandle handle = getPlugin(call.getPluginId()); - - if (handle != null) { - Bundle bundle = handle.getInstance().saveInstanceState(); - if (bundle != null) { - outState.putString(BUNDLE_LAST_PLUGIN_ID_KEY, call.getPluginId()); - outState.putString(BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY, call.getMethodName()); - outState.putString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY, call.getData().toString()); - outState.putBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY, bundle); - } else { - Logger.error("Couldn't save last " + call.getPluginId() + "'s Plugin " + call.getMethodName() + " call"); - } - } - } - } - - @Deprecated - @SuppressWarnings("deprecation") - public void startActivityForPluginWithResult(PluginCall call, Intent intent, int requestCode) { - Logger.debug("Starting activity for result"); - - pluginCallForLastActivity = call; - - getActivity().startActivityForResult(intent, requestCode); - } - - /** - * Check for legacy Capacitor or Cordova plugins that may have registered to handle a permission - * request, and handle them if so. If not handled, false is returned. - * - * @param requestCode the code that was requested - * @param permissions the permissions requested - * @param grantResults the set of granted/denied permissions - * @return true if permission code was handled by a plugin explicitly, false if not - */ - @SuppressWarnings("deprecation") - boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - PluginHandle plugin = getPluginWithRequestCode(requestCode); - - if (plugin == null) { - boolean permissionHandled = false; - Logger.debug("Unable to find a Capacitor plugin to handle permission requestCode, trying Cordova plugins " + requestCode); - try { - permissionHandled = cordovaInterface.handlePermissionResult(requestCode, permissions, grantResults); - } catch (JSONException e) { - Logger.debug("Error on Cordova plugin permissions request " + e.getMessage()); - } - return permissionHandled; - } - - // Call deprecated method if using deprecated NativePlugin annotation - if (plugin.getPluginAnnotation() == null) { - plugin.getInstance().handleRequestPermissionsResult(requestCode, permissions, grantResults); - return true; - } - - return false; - } - - /** - * Saves permission states and rejects if permissions were not correctly defined in - * the AndroidManifest.xml file. - * - * @param plugin - * @param savedCall - * @param permissions - * @return true if permissions were saved and defined correctly, false if not - */ - protected boolean validatePermissions(Plugin plugin, PluginCall savedCall, Map permissions) { - SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE); - - for (Map.Entry permission : permissions.entrySet()) { - String permString = permission.getKey(); - boolean isGranted = permission.getValue(); - - if (isGranted) { - // Permission granted. If previously denied, remove cached state - String state = prefs.getString(permString, null); - - if (state != null) { - SharedPreferences.Editor editor = prefs.edit(); - editor.remove(permString); - editor.apply(); - } - } else { - SharedPreferences.Editor editor = prefs.edit(); - - if (ActivityCompat.shouldShowRequestPermissionRationale(getActivity(), permString)) { - // Permission denied, can prompt again with rationale - editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString()); - } else { - // Permission denied permanently, store this state for future reference - editor.putString(permString, PermissionState.DENIED.toString()); - } - - editor.apply(); - } - } - - String[] permStrings = permissions.keySet().toArray(new String[0]); - - if (!PermissionHelper.hasDefinedPermissions(getContext(), permStrings)) { - StringBuilder builder = new StringBuilder(); - builder.append("Missing the following permissions in AndroidManifest.xml:\n"); - String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permStrings); - for (String perm : missing) { - builder.append(perm + "\n"); - } - savedCall.reject(builder.toString()); - return false; - } - - return true; - } - - /** - * Helper to check all permissions and see the current states of each permission. - * - * @since 3.0.0 - * @return A mapping of permission aliases to the associated granted status. - */ - protected Map getPermissionStates(Plugin plugin) { - Map permissionsResults = new HashMap<>(); - CapacitorPlugin annotation = plugin.getPluginHandle().getPluginAnnotation(); - for (Permission perm : annotation.permissions()) { - // If a permission is defined with no permission constants, return GRANTED for it. - // Otherwise, get its true state. - if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) { - String key = perm.alias(); - if (!key.isEmpty()) { - PermissionState existingResult = permissionsResults.get(key); - - // auto set permission state to GRANTED if the alias is empty. - if (existingResult == null) { - permissionsResults.put(key, PermissionState.GRANTED); - } - } - } else { - for (String permString : perm.strings()) { - String key = perm.alias().isEmpty() ? permString : perm.alias(); - PermissionState permissionStatus; - if (ActivityCompat.checkSelfPermission(this.getContext(), permString) == PackageManager.PERMISSION_GRANTED) { - permissionStatus = PermissionState.GRANTED; - } else { - permissionStatus = PermissionState.PROMPT; - - // Check if there is a cached permission state for the "Never ask again" state - SharedPreferences prefs = getContext().getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE); - String state = prefs.getString(permString, null); - - if (state != null) { - permissionStatus = PermissionState.byState(state); - } - } - - PermissionState existingResult = permissionsResults.get(key); - - // multiple permissions with the same alias must all be true, otherwise all false. - if (existingResult == null || existingResult == PermissionState.GRANTED) { - permissionsResults.put(key, permissionStatus); - } - } - } - } - - return permissionsResults; - } - - /** - * Handle an activity result and pass it to a plugin that has indicated it wants to - * handle the result. - * @param requestCode - * @param resultCode - * @param data - */ - @SuppressWarnings("deprecation") - boolean onActivityResult(int requestCode, int resultCode, Intent data) { - PluginHandle plugin = getPluginWithRequestCode(requestCode); - - if (plugin == null || plugin.getInstance() == null) { - Logger.debug("Unable to find a Capacitor plugin to handle requestCode, trying Cordova plugins " + requestCode); - return cordovaInterface.onActivityResult(requestCode, resultCode, data); - } - - // deprecated, to be removed - PluginCall lastCall = plugin.getInstance().getSavedCall(); - - // If we don't have a saved last call (because our app was killed and restarted, for example), - // Then we should see if we have any saved plugin call information and generate a new, - // "dangling" plugin call (a plugin call that doesn't have a corresponding web callback) - // and then send that to the plugin - if (lastCall == null && pluginCallForLastActivity != null) { - plugin.getInstance().saveCall(pluginCallForLastActivity); - } - - plugin.getInstance().handleOnActivityResult(requestCode, resultCode, data); - - // Clear the plugin call we may have re-hydrated on app launch - pluginCallForLastActivity = null; - - return true; - } - - /** - * Handle an onNewIntent lifecycle event and notify the plugins - * @param intent - */ - public void onNewIntent(Intent intent) { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnNewIntent(intent); - } - - if (cordovaWebView != null) { - cordovaWebView.onNewIntent(intent); - } - } - - /** - * Handle an onConfigurationChanged event and notify the plugins - * @param newConfig - */ - public void onConfigurationChanged(Configuration newConfig) { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnConfigurationChanged(newConfig); - } - } - - /** - * Handle onRestart lifecycle event and notify the plugins - */ - public void onRestart() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnRestart(); - } - } - - /** - * Handle onStart lifecycle event and notify the plugins - */ - public void onStart() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnStart(); - } - - if (cordovaWebView != null) { - cordovaWebView.handleStart(); - } - } - - /** - * Handle onResume lifecycle event and notify the plugins - */ - public void onResume() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnResume(); - } - - if (cordovaWebView != null) { - cordovaWebView.handleResume(this.shouldKeepRunning()); - } - } - - /** - * Handle onPause lifecycle event and notify the plugins - */ - public void onPause() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnPause(); - } - - if (cordovaWebView != null) { - boolean keepRunning = this.shouldKeepRunning() || cordovaInterface.getActivityResultCallback() != null; - cordovaWebView.handlePause(keepRunning); - } - } - - /** - * Handle onStop lifecycle event and notify the plugins - */ - public void onStop() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnStop(); - } - - if (cordovaWebView != null) { - cordovaWebView.handleStop(); - } - } - - /** - * Handle onDestroy lifecycle event and notify the plugins - */ - public void onDestroy() { - for (PluginHandle plugin : plugins.values()) { - plugin.getInstance().handleOnDestroy(); - } - - handlerThread.quitSafely(); - - if (cordovaWebView != null) { - cordovaWebView.handleDestroy(); - } - } - - /** - * Handle onDetachedFromWindow lifecycle event - */ - public void onDetachedFromWindow() { - webView.removeAllViews(); - webView.destroy(); - } - - public String getServerBasePath() { - return this.localServer.getBasePath(); - } - - /** - * Tell the local server to load files from the given - * file path instead of the assets path. - * @param path - */ - public void setServerBasePath(String path) { - localServer.hostFiles(path); - webView.post(() -> webView.loadUrl(appUrl)); - } - - /** - * Tell the local server to load files from the given - * asset path. - * @param path - */ - public void setServerAssetPath(String path) { - localServer.hostAssets(path); - webView.post(() -> webView.loadUrl(appUrl)); - } - - /** - * Reload the WebView - */ - public void reload() { - webView.post(() -> webView.loadUrl(appUrl)); - } - - public String getLocalUrl() { - return localUrl; - } - - public WebViewLocalServer getLocalServer() { - return localServer; - } - - public HostMask getAppAllowNavigationMask() { - return appAllowNavigationMask; - } - - public Set getAllowedOriginRules() { - return allowedOriginRules; - } - - public BridgeWebViewClient getWebViewClient() { - return this.webViewClient; - } - - public void setWebViewClient(BridgeWebViewClient client) { - this.webViewClient = client; - webView.setWebViewClient(client); - } - - List getWebViewListeners() { - return webViewListeners; - } - - void setWebViewListeners(List webViewListeners) { - this.webViewListeners = webViewListeners; - } - - RouteProcessor getRouteProcessor() { - return routeProcessor; - } - - void setRouteProcessor(RouteProcessor routeProcessor) { - this.routeProcessor = routeProcessor; - } - - ServerPath getServerPath() { - return serverPath; - } - - /** - * Add a listener that the WebViewClient can trigger on certain events. - * @param webViewListener A {@link WebViewListener} to add. - */ - public void addWebViewListener(WebViewListener webViewListener) { - webViewListeners.add(webViewListener); - } - - /** - * Remove a listener that the WebViewClient triggers on certain events. - * @param webViewListener A {@link WebViewListener} to remove. - */ - public void removeWebViewListener(WebViewListener webViewListener) { - webViewListeners.remove(webViewListener); - } - - public static class Builder { - - private Bundle instanceState = null; - private CapConfig config = null; - private List> plugins = new ArrayList<>(); - private List pluginInstances = new ArrayList<>(); - private AppCompatActivity activity; - private Fragment fragment; - private RouteProcessor routeProcessor; - private final List webViewListeners = new ArrayList<>(); - private ServerPath serverPath; - - public Builder(AppCompatActivity activity) { - this.activity = activity; - } - - public Builder(Fragment fragment) { - this.activity = (AppCompatActivity) fragment.getActivity(); - this.fragment = fragment; - } - - public Builder setInstanceState(Bundle instanceState) { - this.instanceState = instanceState; - return this; - } - - public Builder setConfig(CapConfig config) { - this.config = config; - return this; - } - - public Builder setPlugins(List> plugins) { - this.plugins = plugins; - return this; - } - - public Builder addPlugin(Class plugin) { - this.plugins.add(plugin); - return this; - } - - public Builder addPlugins(List> plugins) { - for (Class cls : plugins) { - this.addPlugin(cls); - } - - return this; - } - - public Builder addPluginInstance(Plugin plugin) { - this.pluginInstances.add(plugin); - return this; - } - - public Builder addPluginInstances(List plugins) { - this.pluginInstances.addAll(plugins); - return this; - } - - public Builder addWebViewListener(WebViewListener webViewListener) { - webViewListeners.add(webViewListener); - return this; - } - - public Builder addWebViewListeners(List webViewListeners) { - for (WebViewListener listener : webViewListeners) { - this.addWebViewListener(listener); - } - - return this; - } - - public Builder setRouteProcessor(RouteProcessor routeProcessor) { - this.routeProcessor = routeProcessor; - return this; - } - - public Builder setServerPath(ServerPath serverPath) { - this.serverPath = serverPath; - return this; - } - - public Bridge create() { - // Cordova initialization - ConfigXmlParser parser = new ConfigXmlParser(); - parser.parse(activity.getApplicationContext()); - CordovaPreferences preferences = parser.getPreferences(); - preferences.setPreferencesBundle(activity.getIntent().getExtras()); - List pluginEntries = parser.getPluginEntries(); - - MockCordovaInterfaceImpl cordovaInterface = new MockCordovaInterfaceImpl(activity); - if (instanceState != null) { - cordovaInterface.restoreInstanceState(instanceState); - } - - WebView webView = this.fragment != null ? fragment.getView().findViewById(R.id.webview) : activity.findViewById(R.id.webview); - MockCordovaWebViewImpl mockWebView = new MockCordovaWebViewImpl(activity.getApplicationContext()); - mockWebView.init(cordovaInterface, pluginEntries, preferences, webView); - PluginManager pluginManager = mockWebView.getPluginManager(); - cordovaInterface.onCordovaInit(pluginManager); - - // Bridge initialization - Bridge bridge = new Bridge( - activity, - serverPath, - fragment, - webView, - plugins, - pluginInstances, - cordovaInterface, - pluginManager, - preferences, - config - ); - - if (webView instanceof CapacitorWebView) { - CapacitorWebView capacitorWebView = (CapacitorWebView) webView; - capacitorWebView.setBridge(bridge); - } - - bridge.setCordovaWebView(mockWebView); - bridge.setWebViewListeners(webViewListeners); - bridge.setRouteProcessor(routeProcessor); - - if (instanceState != null) { - bridge.restoreInstanceState(instanceState); - } - - return bridge; - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.kt new file mode 100644 index 00000000..93b463ef --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Bridge.kt @@ -0,0 +1,1561 @@ +package com.getcapacitor + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.HandlerThread +import android.webkit.ValueCallback +import android.webkit.WebSettings +import android.webkit.WebView +import androidx.activity.result.ActivityResultCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContract +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.pm.PackageInfoCompat +import androidx.fragment.app.Fragment +import com.getcapacitor.android.R +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.cordova.MockCordovaInterfaceImpl +import com.getcapacitor.cordova.MockCordovaWebViewImpl +import com.getcapacitor.plugin.CapacitorCookies +import com.getcapacitor.plugin.CapacitorHttp +import com.getcapacitor.util.HostMask +import com.getcapacitor.util.InternalUtils +import com.getcapacitor.util.PermissionHelper +import com.getcapacitor.util.WebColor +import org.apache.cordova.ConfigXmlParser +import org.apache.cordova.CordovaPreferences +import org.apache.cordova.CordovaWebView +import org.apache.cordova.PluginEntry +import org.apache.cordova.PluginManager +import org.json.JSONException +import java.io.File +import java.net.SocketTimeoutException +import java.net.URL +import java.util.Arrays +import java.util.LinkedList +import java.util.regex.Pattern + +/** + * The Bridge class is the main engine of Capacitor. It manages + * loading and communicating with all Plugins, + * proxying Native events to Plugins, executing Plugin methods, + * communicating with the WebView, and a whole lot more. + * + * Generally, you'll not use Bridge directly, instead, extend from BridgeActivity + * to get a WebView instance and proxy native events automatically. + * + * If you want to use this Bridge in an existing Android app, please + * see the source for BridgeActivity for the methods you'll need to + * pass through to Bridge: + * [ + * BridgeActivity.java](https://github.com/ionic-team/capacitor/blob/HEAD/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java) + */ +class Bridge private constructor( + context: AppCompatActivity?, + serverPath: ServerPath?, + fragment: Fragment?, + webView: WebView, + initialPlugins: List>, + pluginInstances: List, + cordovaInterface: MockCordovaInterfaceImpl, + pluginManager: PluginManager, + preferences: CordovaPreferences, + config: CapConfig? +) { + // Loaded Capacitor config + val config: CapConfig + + /** + * Get the activity for the app + * @return + */ + // A reference to the main activity for the app + val activity: AppCompatActivity? + + /** + * Get the fragment for the app, if applicable. This will likely be null unless Capacitor + * is being used embedded in a Native Android app. + * + * @return The fragment containing the Capacitor WebView. + */ + // A reference to the containing Fragment if used + val fragment: Fragment? + var localServer: WebViewLocalServer? = null + private set + var localUrl: String? = null + private set + var appUrl: String? = null + private set + private var appUrlConfig: String? = null + var appAllowNavigationMask: HostMask? = null + private set + private val allowedOriginRules: MutableSet = HashSet() + private val authorities = ArrayList() + + /** + * Get the core WebView under Capacitor's control + * @return + */ + // A reference to the main WebView for the app + val webView: WebView + val cordovaInterface: MockCordovaInterfaceImpl + private var cordovaWebView: CordovaWebView? = null + private val preferences: CordovaPreferences + private var webViewClient: BridgeWebViewClient + val app: App = App() + + // Our MessageHandler for sending and receiving data to the WebView + private val msgHandler: MessageHandler + + // The ThreadHandler for executing plugin calls + private val handlerThread = HandlerThread("CapacitorPlugins") + + // Our Handler for posting plugin calls. Created from the ThreadHandler + private var taskHandler: Handler? = null + + private val initialPlugins: List> + + private val pluginInstances: List + + // A map of Plugin Id's to PluginHandle's + private val plugins: MutableMap = HashMap() + + // Stored plugin calls that we're keeping around to call again someday + private var savedCalls: MutableMap = HashMap() + + // The call IDs of saved plugin calls with associated plugin id for handling permissions + private val savedPermissionCallIds: MutableMap> = HashMap() + + // Store a plugin that started a new activity, in case we need to resume + // the app and return that data back + private var pluginCallForLastActivity: PluginCall? = null + + /** + * Get the URI that was used to launch the app (if any) + * @return + */ + // Any URI that was passed to the app on start + val intentUri: Uri? + + // A list of listeners that trigger when webView events occur + private var webViewListeners: MutableList = ArrayList() + + // An interface to manipulate route resolving + var routeProcessor: RouteProcessor? = null + + // A pre-determined path to load the bridge + val serverPath: ServerPath? + + /** + * Create the Bridge with a reference to the main [Activity] for the + * app, and a reference to the [WebView] our app will use. + * @param context + * @param webView + */ + @Deprecated("Use {@link Bridge.Builder} to create Bridge instances") + constructor( + context: AppCompatActivity?, + webView: WebView, + initialPlugins: List>, + cordovaInterface: MockCordovaInterfaceImpl, + pluginManager: PluginManager, + preferences: CordovaPreferences, + config: CapConfig? + ) : this( + context, + null, + null, + webView, + initialPlugins, + ArrayList(), + cordovaInterface, + pluginManager, + preferences, + config + ) + + init { + this.serverPath = serverPath + this.activity = context + this.fragment = fragment + this.webView = webView + this.webViewClient = BridgeWebViewClient(this) + this.initialPlugins = initialPlugins + this.pluginInstances = pluginInstances + this.cordovaInterface = cordovaInterface + this.preferences = preferences + + // Start our plugin execution threads and handlers + handlerThread.start() + taskHandler = Handler(handlerThread.looper) + + this.config = config ?: CapConfig.loadDefault(activity) + Logger.init(this.config) + + // Initialize web view and message handler for it + this.initWebView() + this.setAllowedOriginRules() + this.msgHandler = MessageHandler(this, webView, pluginManager) + + // Grab any intent info that our app was launched with + val intent = context!!.intent + this.intentUri = intent.data + // Register our core plugins + this.registerAllPlugins() + + this.loadWebView() + } + + private fun setAllowedOriginRules() { + val appAllowNavigationConfig = config.allowNavigation + val authority = this.host + val scheme = this.scheme + allowedOriginRules.add("$scheme://$authority") + if (this.serverUrl != null) { + allowedOriginRules.add(this.serverUrl) + } + if (appAllowNavigationConfig != null) { + for (allowNavigation in appAllowNavigationConfig) { + if (!allowNavigation.startsWith("http")) { + allowedOriginRules.add("https://$allowNavigation") + } else { + allowedOriginRules.add(allowNavigation) + } + } + authorities.addAll(Arrays.asList(*appAllowNavigationConfig)) + } + this.appAllowNavigationMask = HostMask.Parser.parse(appAllowNavigationConfig) + } + + private fun loadWebView() { + val html5mode = config.isHTML5Mode + + // Start the local web server + localServer = WebViewLocalServer(activity, this, jSInjector, authorities, html5mode) + localServer!!.hostAssets(DEFAULT_WEB_ASSET_DIR) + + Logger.debug("Loading app at $appUrl") + + webView.webChromeClient = BridgeWebChromeClient(this) + webView.webViewClient = webViewClient + + if (!isDeployDisabled && !isNewBinary) { + val prefs = getContext() + .getSharedPreferences( + com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, + Activity.MODE_PRIVATE + ) + val path = prefs.getString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, null) + if (path != null && !path.isEmpty() && File(path).exists()) { + serverBasePath = path + } + } + if (!this.isMinimumWebViewInstalled) { + val errorUrl = this.errorUrl + if (errorUrl != null) { + webView.loadUrl(errorUrl) + return + } else { + Logger.error(MINIMUM_ANDROID_WEBVIEW_ERROR) + } + } + + // If serverPath configured, start server based on provided path + if (serverPath != null) { + if (serverPath.type == ServerPath.PathType.ASSET_PATH) { + setServerAssetPath(serverPath.path) + } else { + serverBasePath = serverPath.path + } + } else { + // Get to work + webView.loadUrl(appUrl!!) + } + } + + @get:SuppressLint("WebViewApiAvailability") + val isMinimumWebViewInstalled: Boolean + get() { + val pm = getContext()!!.packageManager + + // Check getCurrentWebViewPackage() directly if above Android 8 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val info = WebView.getCurrentWebViewPackage() + val pattern = Pattern.compile("(\\d+)") + val matcher = pattern.matcher(info!!.versionName) + if (matcher.find()) { + val majorVersionStr = matcher.group(0) + val majorVersion = majorVersionStr.toInt() + if (info.packageName == "com.huawei.webview") { + return majorVersion >= config.minHuaweiWebViewVersion + } + return majorVersion >= config.minWebViewVersion + } else { + return false + } + } + + // Otherwise manually check WebView versions + try { + var webViewPackage = "com.google.android.webview" + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + webViewPackage = "com.android.chrome" + } + val info = InternalUtils.getPackageInfo(pm, webViewPackage) + val majorVersionStr = + info.versionName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + val majorVersion = majorVersionStr.toInt() + return majorVersion >= config.minWebViewVersion + } catch (ex: Exception) { + Logger.warn("Unable to get package info for 'com.google.android.webview'$ex") + } + + try { + val info = InternalUtils.getPackageInfo(pm, "com.android.webview") + val majorVersionStr = + info.versionName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + val majorVersion = majorVersionStr.toInt() + return majorVersion >= config.minWebViewVersion + } catch (ex: Exception) { + Logger.warn("Unable to get package info for 'com.android.webview'$ex") + } + + val amazonFireMajorWebViewVersion = + extractWebViewMajorVersion(pm, "com.amazon.webview.chromium") + if (amazonFireMajorWebViewVersion >= config.minWebViewVersion) { + return true + } + + // Could not detect any webview, return false + return false + } + + private fun extractWebViewMajorVersion(pm: PackageManager, webViewPackageName: String): Int { + try { + val info = InternalUtils.getPackageInfo(pm, webViewPackageName) + val majorVersionStr = + info.versionName.split("\\.".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray()[0] + val majorVersion = majorVersionStr.toInt() + return majorVersion + } catch (ex: Exception) { + Logger.warn( + String.format( + "Unable to get package info for '%s' with err '%s'", + webViewPackageName, + ex + ) + ) + } + return 0 + } + + fun launchIntent(url: Uri): Boolean { + /* + * Give plugins the chance to handle the url + */ + for ((_, value) in plugins) { + val plugin = value.instance + if (plugin != null) { + val shouldOverrideLoad = plugin.shouldOverrideLoad(url) + if (shouldOverrideLoad != null) { + return shouldOverrideLoad + } + } + } + + if (url.scheme == "data" || url.scheme == "blob") { + return false + } + + val appUri = Uri.parse(appUrl) + if (!(appUri.host == url.host && url.scheme == appUri.scheme) && + !appAllowNavigationMask!!.matches(url.host) + ) { + try { + val openIntent = Intent(Intent.ACTION_VIEW, url) + getContext()!!.startActivity(openIntent) + } catch (e: ActivityNotFoundException) { + // TODO - trigger an event + } + return true + } + return false + } + + private val isNewBinary: Boolean + get() { + var versionCode = "" + var versionName = "" + val prefs = getContext() + .getSharedPreferences( + com.getcapacitor.plugin.WebView.WEBVIEW_PREFS_NAME, + Activity.MODE_PRIVATE + ) + val lastVersionCode = prefs.getString(LAST_BINARY_VERSION_CODE, null) + val lastVersionName = prefs.getString(LAST_BINARY_VERSION_NAME, null) + + try { + val pm = getContext()!!.packageManager + val pInfo = InternalUtils.getPackageInfo(pm, getContext()!!.packageName) + versionCode = PackageInfoCompat.getLongVersionCode(pInfo).toInt() + .toString() + versionName = pInfo.versionName + } catch (ex: Exception) { + Logger.error("Unable to get package info", ex) + } + + if (versionCode != lastVersionCode || versionName != lastVersionName) { + val editor = prefs.edit() + editor.putString(LAST_BINARY_VERSION_CODE, versionCode) + editor.putString(LAST_BINARY_VERSION_NAME, versionName) + editor.putString(com.getcapacitor.plugin.WebView.CAP_SERVER_PATH, "") + editor.apply() + return true + } + return false + } + + val isDeployDisabled: Boolean + get() = preferences.getBoolean("DisableDeploy", false) + + fun shouldKeepRunning(): Boolean { + return preferences.getBoolean("KeepRunning", true) + } + + fun handleAppUrlLoadError(ex: Exception?) { + if (ex is SocketTimeoutException) { + Logger.error( + "Unable to load app. Ensure the server is running at " + + appUrl + + ", or modify the " + + "appUrl setting in capacitor.config.json (make sure to npx cap copy after to commit changes).", + ex + ) + } + } + + val isDevMode: Boolean + get() = (activity!!.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + + protected fun setCordovaWebView(cordovaWebView: CordovaWebView?) { + this.cordovaWebView = cordovaWebView + } + + /** + * Get the Context for the App + * @return + */ + fun getContext(): Context? { + return this.activity + } + + val scheme: String + /** + * Get scheme that is used to serve content + * @return + */ + get() = config.androidScheme + + val host: String + /** + * Get host name that is used to serve content + * @return + */ + get() = config.hostname + + val serverUrl: String? + /** + * Get the server url that is used to serve content + * @return + */ + get() = config.serverUrl + + val errorUrl: String? + get() { + val errorPath = config.errorPath + + if (errorPath != null && !errorPath.trim { it <= ' ' }.isEmpty()) { + val authority = this.host + val scheme = this.scheme + + val localUrl = "$scheme://$authority" + + return "$localUrl/$errorPath" + } + + return null + } + + fun reset() { + savedCalls = HashMap() + } + + /** + * Initialize the WebView, setting required flags + */ + @SuppressLint("SetJavaScriptEnabled") + private fun initWebView() { + val settings = webView.settings + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + settings.setGeolocationEnabled(true) + settings.databaseEnabled = true + settings.mediaPlaybackRequiresUserGesture = false + settings.javaScriptCanOpenWindowsAutomatically = true + if (config.isMixedContentAllowed) { + settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW + } + + val appendUserAgent = config.appendedUserAgentString + if (appendUserAgent != null) { + val defaultUserAgent = settings.userAgentString + settings.userAgentString = "$defaultUserAgent $appendUserAgent" + } + val overrideUserAgent = config.overriddenUserAgentString + if (overrideUserAgent != null) { + settings.userAgentString = overrideUserAgent + } + + val backgroundColor = config.backgroundColor + try { + if (backgroundColor != null) { + webView.setBackgroundColor(WebColor.parseColor(backgroundColor)) + } + } catch (ex: IllegalArgumentException) { + Logger.debug("WebView background color not applied") + } + + if (config.isInitialFocus) { + webView.requestFocusFromTouch() + } + + WebView.setWebContentsDebuggingEnabled(config.isWebContentsDebuggingEnabled) + + appUrlConfig = this.serverUrl + val authority = this.host + authorities.add(authority) + val scheme = this.scheme + + localUrl = "$scheme://$authority" + + if (appUrlConfig != null) { + try { + val appUrlObject = URL(appUrlConfig) + authorities.add(appUrlObject.authority) + } catch (ex: Exception) { + Logger.error("Provided server url is invalid: " + ex.message) + return + } + localUrl = appUrlConfig + appUrl = appUrlConfig + } else { + appUrl = localUrl + // custom URL schemes requires path ending with / + if (scheme != CAPACITOR_HTTP_SCHEME && scheme != CAPACITOR_HTTPS_SCHEME) { + appUrl += "/" + } + } + + val appUrlPath = config.startPath + if (appUrlPath != null && !appUrlPath.trim { it <= ' ' }.isEmpty()) { + appUrl += appUrlPath + } + } + + /** + * Register our core Plugin APIs + */ + private fun registerAllPlugins() { + this.registerPlugin(CapacitorCookies::class.java) + this.registerPlugin(com.getcapacitor.plugin.WebView::class.java) + this.registerPlugin(CapacitorHttp::class.java) + + for (pluginClass in this.initialPlugins) { + this.registerPlugin(pluginClass) + } + + for (plugin in pluginInstances) { + registerPluginInstance(plugin) + } + } + + /** + * Register additional plugins + * @param pluginClasses the plugins to register + */ + fun registerPlugins(pluginClasses: Array>) { + for (plugin in pluginClasses) { + this.registerPlugin(plugin) + } + } + + fun registerPluginInstances(pluginInstances: Array) { + for (plugin in pluginInstances) { + this.registerPluginInstance(plugin) + } + } + + @Suppress("deprecation") + private fun getLegacyPluginName(pluginClass: Class): String? { + val legacyPluginAnnotation = pluginClass.getAnnotation( + NativePlugin::class.java + ) + if (legacyPluginAnnotation == null) { + Logger.error("Plugin doesn't have the @CapacitorPlugin annotation. Please add it") + return null + } + + return legacyPluginAnnotation.name + } + + /** + * Register a plugin class + * @param pluginClass a class inheriting from Plugin + */ + fun registerPlugin(pluginClass: Class) { + val pluginId = pluginId(pluginClass) ?: return + + try { + plugins[pluginId] = PluginHandle(this, pluginClass) + } catch (ex: InvalidPluginException) { + logInvalidPluginException(pluginClass) + } catch (ex: PluginLoadException) { + logPluginLoadException(pluginClass, ex) + } + } + + fun registerPluginInstance(plugin: Plugin) { + val clazz: Class = plugin.javaClass + val pluginId = pluginId(clazz) ?: return + + try { + plugins[pluginId] = PluginHandle(this, plugin) + } catch (ex: InvalidPluginException) { + logInvalidPluginException(clazz) + } + } + + private fun pluginId(clazz: Class): String? { + val pluginName = pluginName(clazz) + var pluginId = clazz.simpleName + if (pluginName == null) return null + + if (pluginName != "") { + pluginId = pluginName + } + Logger.debug("Registering plugin instance: $pluginId") + return pluginId + } + + private fun pluginName(clazz: Class): String? { + val pluginName: String? + val pluginAnnotation = clazz.getAnnotation( + CapacitorPlugin::class.java + ) + pluginName = pluginAnnotation?.name ?: getLegacyPluginName(clazz) + + return pluginName + } + + private fun logInvalidPluginException(clazz: Class) { + Logger.error( + "NativePlugin " + + clazz.name + + " is invalid. Ensure the @CapacitorPlugin annotation exists on the plugin class and" + + " the class extends Plugin" + ) + } + + private fun logPluginLoadException(clazz: Class, ex: Exception) { + Logger.error("NativePlugin " + clazz.name + " failed to load", ex) + } + + fun getPlugin(pluginId: String): PluginHandle? { + return plugins[pluginId] + } + + /** + * Find the plugin handle that responds to the given request code. This will + * fire after certain Android OS intent results/permission checks/etc. + * @param requestCode + * @return + */ + @Deprecated("") + @Suppress("deprecation") + fun getPluginWithRequestCode(requestCode: Int): PluginHandle? { + for (handle in plugins.values) { + var requestCodes: IntArray + + val pluginAnnotation = handle.pluginAnnotation + if (pluginAnnotation == null) { + // Check for legacy plugin annotation, @NativePlugin + val legacyPluginAnnotation = handle.legacyPluginAnnotation ?: continue + + if (legacyPluginAnnotation.permissionRequestCode == requestCode) { + return handle + } + + requestCodes = legacyPluginAnnotation.requestCodes + + for (rc in requestCodes) { + if (rc == requestCode) { + return handle + } + } + } else { + requestCodes = pluginAnnotation.requestCodes + + for (rc in requestCodes) { + if (rc == requestCode) { + return handle + } + } + } + } + return null + } + + /** + * Call a method on a plugin. + * @param pluginId the plugin id to use to lookup the plugin handle + * @param methodName the name of the method to call + * @param call the call object to pass to the method + */ + fun callPluginMethod(pluginId: String, methodName: String, call: PluginCall) { + try { + val plugin = this.getPlugin(pluginId) + + if (plugin == null) { + Logger.error("unable to find plugin : $pluginId") + call.errorCallback("unable to find plugin : $pluginId") + return + } + + if (Logger.shouldLog()) { + Logger.verbose( + "callback: " + + call.callbackId + + ", pluginId: " + + plugin.id + + ", methodName: " + + methodName + + ", methodData: " + + call.data.toString() + ) + } + + val currentThreadTask = Runnable { + try { + plugin.invoke(methodName, call) + + if (call.isKeptAlive) { + saveCall(call) + } + } catch (ex: PluginLoadException) { + Logger.error("Unable to execute plugin method", ex) + } catch (ex: InvalidPluginMethodException) { + Logger.error("Unable to execute plugin method", ex) + } catch (ex: Exception) { + Logger.error("Serious error executing plugin", ex) + throw RuntimeException(ex) + } + } + + taskHandler!!.post(currentThreadTask) + } catch (ex: Exception) { + Logger.error(Logger.tags("callPluginMethod"), "error : $ex", null) + call.errorCallback(ex.toString()) + } + } + + /** + * Evaluate JavaScript in the web view. This method + * executes on the main thread automatically. + * @param js the JS to execute + * @param callback an optional ValueCallback that will synchronously receive a value + * after calling the JS + */ + fun eval(js: String?, callback: ValueCallback?) { + val mainHandler = Handler(activity!!.mainLooper) + mainHandler.post { webView.evaluateJavascript(js!!, callback) } + } + + @JvmOverloads + fun logToJs(message: String, level: String = "log") { + eval("window.Capacitor.logJs(\"$message\", \"$level\")", null) + } + + fun triggerJSEvent(eventName: String, target: String) { + eval("window.Capacitor.triggerEvent(\"$eventName\", \"$target\")") { s: String? -> } + } + + fun triggerJSEvent(eventName: String, target: String, data: String) { + eval("window.Capacitor.triggerEvent(\"$eventName\", \"$target\", $data)") { s: String? -> } + } + + fun triggerWindowJSEvent(eventName: String) { + this.triggerJSEvent(eventName, "window") + } + + fun triggerWindowJSEvent(eventName: String, data: String) { + this.triggerJSEvent(eventName, "window", data) + } + + fun triggerDocumentJSEvent(eventName: String) { + this.triggerJSEvent(eventName, "document") + } + + fun triggerDocumentJSEvent(eventName: String, data: String) { + this.triggerJSEvent(eventName, "document", data) + } + + fun execute(runnable: Runnable?) { + taskHandler!!.post(runnable!!) + } + + fun executeOnMainThread(runnable: Runnable?) { + val mainHandler = Handler(activity!!.mainLooper) + + mainHandler.post(runnable!!) + } + + /** + * Retain a call between plugin invocations + * @param call + */ + fun saveCall(call: PluginCall) { + savedCalls[call.callbackId] = call + } + + /** + * Get a retained plugin call + * @param callbackId the callbackId to use to lookup the call with + * @return the stored call + */ + fun getSavedCall(callbackId: String?): PluginCall? { + if (callbackId == null) { + return null + } + + return savedCalls[callbackId] + } + + fun getPluginCallForLastActivity(): PluginCall? { + val pluginCallForLastActivity = this.pluginCallForLastActivity + this.pluginCallForLastActivity = null + return pluginCallForLastActivity + } + + fun setPluginCallForLastActivity(pluginCallForLastActivity: PluginCall?) { + this.pluginCallForLastActivity = pluginCallForLastActivity + } + + /** + * Release a retained call + * @param call a call to release + */ + fun releaseCall(call: PluginCall) { + releaseCall(call.callbackId) + } + + /** + * Release a retained call by its ID + * @param callbackId an ID of a callback to release + */ + fun releaseCall(callbackId: String) { + savedCalls.remove(callbackId) + } + + /** + * Removes the earliest saved call prior to a permissions request for a given plugin and + * returns it. + * + * @return The saved plugin call + */ + fun getPermissionCall(pluginId: String): PluginCall? { + val permissionCallIds = savedPermissionCallIds[pluginId] + var savedCallId: String? = null + if (permissionCallIds != null) { + savedCallId = permissionCallIds.poll() + } + + return getSavedCall(savedCallId) + } + + /** + * Save a call to be retrieved after requesting permissions. Calls are saved in order. + * + * @param call The plugin call to save. + */ + fun savePermissionCall(call: PluginCall?) { + if (call != null) { + if (!savedPermissionCallIds.containsKey(call.pluginId)) { + savedPermissionCallIds[call.pluginId] = LinkedList() + } + + savedPermissionCallIds[call.pluginId]!!.add(call.callbackId) + saveCall(call) + } + } + + /** + * Register an Activity Result Launcher to the containing Fragment or Activity. + * + * @param contract A contract specifying that an activity can be called with an input of + * type I and produce an output of type O. + * @param callback The callback run on Activity Result. + * @return A registered Activity Result Launcher. + */ + fun registerForActivityResult( + contract: ActivityResultContract, + callback: ActivityResultCallback + ): ActivityResultLauncher { + return fragment?.registerForActivityResult(contract, callback) + ?: activity!!.registerForActivityResult(contract, callback) + } + + private val jSInjector: JSInjector? + /** + * Build the JSInjector that will be used to inject JS into files served to the app, + * to ensure that Capacitor's JS and the JS for all the plugins is loaded each time. + */ + get() { + try { + val globalJS = JSExport.getGlobalJS(activity, config.isLoggingEnabled, isDevMode) + val bridgeJS = JSExport.getBridgeJS(activity) + val pluginJS = JSExport.getPluginJS(plugins.values) + val cordovaJS = JSExport.getCordovaJS(activity) + val cordovaPluginsJS = JSExport.getCordovaPluginJS(activity) + val cordovaPluginsFileJS = JSExport.getCordovaPluginsFileJS(activity) + val localUrlJS = "window.WEBVIEW_SERVER_URL = '$localUrl';" + + return JSInjector( + globalJS, + bridgeJS, + pluginJS, + cordovaJS, + cordovaPluginsJS, + cordovaPluginsFileJS, + localUrlJS + ) + } catch (ex: Exception) { + Logger.error("Unable to export Capacitor JS. App will not function!", ex) + } + return null + } + + /** + * Restore any saved bundle state data + * @param savedInstanceState + */ + fun restoreInstanceState(savedInstanceState: Bundle) { + val lastPluginId = savedInstanceState.getString(BUNDLE_LAST_PLUGIN_ID_KEY) + val lastPluginCallMethod = savedInstanceState.getString( + BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY + ) + val lastOptionsJson = savedInstanceState.getString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY) + + if (lastPluginId != null) { + // If we have JSON blob saved, create a new plugin call with the original options + if (lastOptionsJson != null) { + try { + val options = JSObject(lastOptionsJson) + + pluginCallForLastActivity = + PluginCall( + msgHandler, + lastPluginId, + PluginCall.CALLBACK_ID_DANGLING, + lastPluginCallMethod, + options + ) + } catch (ex: JSONException) { + Logger.error( + "Unable to restore plugin call, unable to parse persisted JSON object", + ex + ) + } + } + + // Let the plugin restore any state it needs + val bundleData = savedInstanceState.getBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY) + val lastPlugin = getPlugin(lastPluginId) + if (bundleData != null && lastPlugin != null) { + lastPlugin.instance.restoreState(bundleData) + } else { + Logger.error("Unable to restore last plugin call") + } + } + } + + fun saveInstanceState(outState: Bundle) { + Logger.debug("Saving instance state!") + + // If there was a last PluginCall for a started activity, we need to + // persist it so we can load it again in case our app gets terminated + if (pluginCallForLastActivity != null) { + val call: PluginCall = pluginCallForLastActivity + val handle = getPlugin(call.pluginId) + + if (handle != null) { + val bundle = handle.instance.saveInstanceState() + if (bundle != null) { + outState.putString(BUNDLE_LAST_PLUGIN_ID_KEY, call.pluginId) + outState.putString(BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY, call.methodName) + outState.putString(BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY, call.data.toString()) + outState.putBundle(BUNDLE_PLUGIN_CALL_BUNDLE_KEY, bundle) + } else { + Logger.error("Couldn't save last " + call.pluginId + "'s Plugin " + call.methodName + " call") + } + } + } + } + + @Deprecated("") + @Suppress("deprecation") + fun startActivityForPluginWithResult(call: PluginCall?, intent: Intent?, requestCode: Int) { + Logger.debug("Starting activity for result") + + pluginCallForLastActivity = call + + activity!!.startActivityForResult(intent!!, requestCode) + } + + /** + * Check for legacy Capacitor or Cordova plugins that may have registered to handle a permission + * request, and handle them if so. If not handled, false is returned. + * + * @param requestCode the code that was requested + * @param permissions the permissions requested + * @param grantResults the set of granted/denied permissions + * @return true if permission code was handled by a plugin explicitly, false if not + */ + @Suppress("deprecation") + fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array?, + grantResults: IntArray? + ): Boolean { + val plugin = getPluginWithRequestCode(requestCode) + + if (plugin == null) { + var permissionHandled = false + Logger.debug("Unable to find a Capacitor plugin to handle permission requestCode, trying Cordova plugins $requestCode") + try { + permissionHandled = + cordovaInterface.handlePermissionResult(requestCode, permissions, grantResults) + } catch (e: JSONException) { + Logger.debug("Error on Cordova plugin permissions request " + e.message) + } + return permissionHandled + } + + // Call deprecated method if using deprecated NativePlugin annotation + if (plugin.pluginAnnotation == null) { + plugin.instance.handleRequestPermissionsResult(requestCode, permissions, grantResults) + return true + } + + return false + } + + /** + * Saves permission states and rejects if permissions were not correctly defined in + * the AndroidManifest.xml file. + * + * @param plugin + * @param savedCall + * @param permissions + * @return true if permissions were saved and defined correctly, false if not + */ + fun validatePermissions( + plugin: Plugin?, + savedCall: PluginCall, + permissions: Map + ): Boolean { + val prefs = + getContext()!!.getSharedPreferences(PERMISSION_PREFS_NAME, Activity.MODE_PRIVATE) + + for ((permString, isGranted) in permissions) { + if (isGranted) { + // Permission granted. If previously denied, remove cached state + val state = prefs.getString(permString, null) + + if (state != null) { + val editor = prefs.edit() + editor.remove(permString) + editor.apply() + } + } else { + val editor = prefs.edit() + + if (ActivityCompat.shouldShowRequestPermissionRationale(activity!!, permString)) { + // Permission denied, can prompt again with rationale + editor.putString(permString, PermissionState.PROMPT_WITH_RATIONALE.toString()) + } else { + // Permission denied permanently, store this state for future reference + editor.putString(permString, PermissionState.DENIED.toString()) + } + + editor.apply() + } + } + + val permStrings = permissions.keys.toTypedArray() + + if (!PermissionHelper.hasDefinedPermissions(getContext(), permStrings)) { + val builder = StringBuilder() + builder.append("Missing the following permissions in AndroidManifest.xml:\n") + val missing = PermissionHelper.getUndefinedPermissions(getContext(), permStrings) + for (perm in missing) { + builder.append(perm + "\n") + } + savedCall.reject(builder.toString()) + return false + } + + return true + } + + /** + * Helper to check all permissions and see the current states of each permission. + * + * @since 3.0.0 + * @return A mapping of permission aliases to the associated granted status. + */ + fun getPermissionStates(plugin: Plugin): Map { + val permissionsResults: MutableMap = HashMap() + val annotation = plugin.pluginHandle.pluginAnnotation + for (perm in annotation.permissions) { + // If a permission is defined with no permission constants, return GRANTED for it. + // Otherwise, get its true state. + if (perm.strings.size == 0 || (perm.strings.size == 1 && perm.strings[0].isEmpty())) { + val key = perm.alias + if (!key.isEmpty()) { + val existingResult = permissionsResults[key] + + // auto set permission state to GRANTED if the alias is empty. + if (existingResult == null) { + permissionsResults[key] = PermissionState.GRANTED + } + } + } else { + for (permString in perm.strings) { + val key = if (perm.alias.isEmpty()) permString else perm.alias + var permissionStatus: PermissionState + if (ActivityCompat.checkSelfPermission( + getContext()!!, permString + ) == PackageManager.PERMISSION_GRANTED + ) { + permissionStatus = PermissionState.GRANTED + } else { + permissionStatus = PermissionState.PROMPT + + // Check if there is a cached permission state for the "Never ask again" state + val prefs = getContext()!!.getSharedPreferences( + PERMISSION_PREFS_NAME, + Activity.MODE_PRIVATE + ) + val state = prefs.getString(permString, null) + + if (state != null) { + permissionStatus = PermissionState.byState(state) + } + } + + val existingResult = permissionsResults[key] + + // multiple permissions with the same alias must all be true, otherwise all false. + if (existingResult == null || existingResult == PermissionState.GRANTED) { + permissionsResults[key] = permissionStatus + } + } + } + } + + return permissionsResults + } + + /** + * Handle an activity result and pass it to a plugin that has indicated it wants to + * handle the result. + * @param requestCode + * @param resultCode + * @param data + */ + @Suppress("deprecation") + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + val plugin = getPluginWithRequestCode(requestCode) + + if (plugin == null || plugin.instance == null) { + Logger.debug("Unable to find a Capacitor plugin to handle requestCode, trying Cordova plugins $requestCode") + return cordovaInterface.onActivityResult(requestCode, resultCode, data) + } + + // deprecated, to be removed + val lastCall = plugin.instance.savedCall + + // If we don't have a saved last call (because our app was killed and restarted, for example), + // Then we should see if we have any saved plugin call information and generate a new, + // "dangling" plugin call (a plugin call that doesn't have a corresponding web callback) + // and then send that to the plugin + if (lastCall == null && pluginCallForLastActivity != null) { + plugin.instance.saveCall(pluginCallForLastActivity) + } + + plugin.instance.handleOnActivityResult(requestCode, resultCode, data) + + // Clear the plugin call we may have re-hydrated on app launch + pluginCallForLastActivity = null + + return true + } + + /** + * Handle an onNewIntent lifecycle event and notify the plugins + * @param intent + */ + fun onNewIntent(intent: Intent?) { + for (plugin in plugins.values) { + plugin.instance.handleOnNewIntent(intent) + } + + if (cordovaWebView != null) { + cordovaWebView!!.onNewIntent(intent) + } + } + + /** + * Handle an onConfigurationChanged event and notify the plugins + * @param newConfig + */ + fun onConfigurationChanged(newConfig: Configuration?) { + for (plugin in plugins.values) { + plugin.instance.handleOnConfigurationChanged(newConfig) + } + } + + /** + * Handle onRestart lifecycle event and notify the plugins + */ + fun onRestart() { + for (plugin in plugins.values) { + plugin.instance.handleOnRestart() + } + } + + /** + * Handle onStart lifecycle event and notify the plugins + */ + fun onStart() { + for (plugin in plugins.values) { + plugin.instance.handleOnStart() + } + + if (cordovaWebView != null) { + cordovaWebView!!.handleStart() + } + } + + /** + * Handle onResume lifecycle event and notify the plugins + */ + fun onResume() { + for (plugin in plugins.values) { + plugin.instance.handleOnResume() + } + + if (cordovaWebView != null) { + cordovaWebView!!.handleResume(this.shouldKeepRunning()) + } + } + + /** + * Handle onPause lifecycle event and notify the plugins + */ + fun onPause() { + for (plugin in plugins.values) { + plugin.instance.handleOnPause() + } + + if (cordovaWebView != null) { + val keepRunning = + this.shouldKeepRunning() || cordovaInterface.activityResultCallback != null + cordovaWebView!!.handlePause(keepRunning) + } + } + + /** + * Handle onStop lifecycle event and notify the plugins + */ + fun onStop() { + for (plugin in plugins.values) { + plugin.instance.handleOnStop() + } + + if (cordovaWebView != null) { + cordovaWebView!!.handleStop() + } + } + + /** + * Handle onDestroy lifecycle event and notify the plugins + */ + fun onDestroy() { + for (plugin in plugins.values) { + plugin.instance.handleOnDestroy() + } + + handlerThread.quitSafely() + + if (cordovaWebView != null) { + cordovaWebView!!.handleDestroy() + } + } + + /** + * Handle onDetachedFromWindow lifecycle event + */ + fun onDetachedFromWindow() { + webView.removeAllViews() + webView.destroy() + } + + var serverBasePath: String? + get() = localServer!!.basePath + /** + * Tell the local server to load files from the given + * file path instead of the assets path. + * @param path + */ + set(path) { + localServer!!.hostFiles(path) + webView.post { webView.loadUrl(appUrl!!) } + } + + /** + * Tell the local server to load files from the given + * asset path. + * @param path + */ + fun setServerAssetPath(path: String?) { + localServer!!.hostAssets(path) + webView.post { webView.loadUrl(appUrl!!) } + } + + /** + * Reload the WebView + */ + fun reload() { + webView.post { webView.loadUrl(appUrl!!) } + } + + fun getAllowedOriginRules(): Set { + return allowedOriginRules + } + + fun getWebViewClient(): BridgeWebViewClient { + return this.webViewClient + } + + fun setWebViewClient(client: BridgeWebViewClient) { + this.webViewClient = client + webView.webViewClient = client + } + + fun getWebViewListeners(): List { + return webViewListeners + } + + fun setWebViewListeners(webViewListeners: MutableList) { + this.webViewListeners = webViewListeners + } + + /** + * Add a listener that the WebViewClient can trigger on certain events. + * @param webViewListener A [WebViewListener] to add. + */ + fun addWebViewListener(webViewListener: WebViewListener) { + webViewListeners.add(webViewListener) + } + + /** + * Remove a listener that the WebViewClient triggers on certain events. + * @param webViewListener A [WebViewListener] to remove. + */ + fun removeWebViewListener(webViewListener: WebViewListener) { + webViewListeners.remove(webViewListener) + } + + class Builder { + private var instanceState: Bundle? = null + private var config: CapConfig? = null + private var plugins: MutableList> = ArrayList() + private val pluginInstances: MutableList = ArrayList() + private var activity: AppCompatActivity? + private var fragment: Fragment? = null + private var routeProcessor: RouteProcessor? = null + private val webViewListeners: MutableList = ArrayList() + private var serverPath: ServerPath? = null + + constructor(activity: AppCompatActivity?) { + this.activity = activity + } + + constructor(fragment: Fragment) { + this.activity = fragment.activity as AppCompatActivity? + this.fragment = fragment + } + + fun setInstanceState(instanceState: Bundle?): Builder { + this.instanceState = instanceState + return this + } + + fun setConfig(config: CapConfig?): Builder { + this.config = config + return this + } + + fun setPlugins(plugins: MutableList>): Builder { + this.plugins = plugins + return this + } + + fun addPlugin(plugin: Class): Builder { + plugins.add(plugin) + return this + } + + fun addPlugins(plugins: List>): Builder { + for (cls in plugins) { + this.addPlugin(cls) + } + + return this + } + + fun addPluginInstance(plugin: Plugin): Builder { + pluginInstances.add(plugin) + return this + } + + fun addPluginInstances(plugins: List?): Builder { + pluginInstances.addAll(plugins!!) + return this + } + + fun addWebViewListener(webViewListener: WebViewListener): Builder { + webViewListeners.add(webViewListener) + return this + } + + fun addWebViewListeners(webViewListeners: List): Builder { + for (listener in webViewListeners) { + this.addWebViewListener(listener) + } + + return this + } + + fun setRouteProcessor(routeProcessor: RouteProcessor?): Builder { + this.routeProcessor = routeProcessor + return this + } + + fun setServerPath(serverPath: ServerPath?): Builder { + this.serverPath = serverPath + return this + } + + fun create(): Bridge { + // Cordova initialization + val parser = ConfigXmlParser() + parser.parse(activity!!.applicationContext) + val preferences = parser.preferences + preferences.setPreferencesBundle(activity!!.intent.extras) + val pluginEntries: List = parser.pluginEntries + + val cordovaInterface = MockCordovaInterfaceImpl(activity) + if (instanceState != null) { + cordovaInterface.restoreInstanceState(instanceState) + } + + val webView = if (this.fragment != null) fragment!!.view!! + .findViewById(R.id.webview) else activity!!.findViewById(R.id.webview) + val mockWebView = MockCordovaWebViewImpl(activity!!.applicationContext) + mockWebView.init(cordovaInterface, pluginEntries, preferences, webView) + val pluginManager = mockWebView.pluginManager + cordovaInterface.onCordovaInit(pluginManager) + + // Bridge initialization + val bridge = Bridge( + activity, + serverPath, + fragment, + webView, + plugins, + pluginInstances, + cordovaInterface, + pluginManager, + preferences, + config + ) + + if (webView is CapacitorWebView) { + webView.setBridge(bridge) + } + + bridge.setCordovaWebView(mockWebView) + bridge.setWebViewListeners(webViewListeners) + bridge.routeProcessor = routeProcessor + + if (instanceState != null) { + bridge.restoreInstanceState(instanceState!!) + } + + return bridge + } + } + + companion object { + private const val PREFS_NAME = "CapacitorSettings" + private const val PERMISSION_PREFS_NAME = "PluginPermStates" + private const val BUNDLE_LAST_PLUGIN_ID_KEY = "capacitorLastActivityPluginId" + private const val BUNDLE_LAST_PLUGIN_CALL_METHOD_NAME_KEY = + "capacitorLastActivityPluginMethod" + private const val BUNDLE_PLUGIN_CALL_OPTIONS_SAVED_KEY = "capacitorLastPluginCallOptions" + private const val BUNDLE_PLUGIN_CALL_BUNDLE_KEY = "capacitorLastPluginCallBundle" + private const val LAST_BINARY_VERSION_CODE = "lastBinaryVersionCode" + private const val LAST_BINARY_VERSION_NAME = "lastBinaryVersionName" + private const val MINIMUM_ANDROID_WEBVIEW_ERROR = "System WebView is not supported" + + // The name of the directory we use to look for index.html and the rest of our web assets + const val DEFAULT_WEB_ASSET_DIR: String = "public" + const val CAPACITOR_HTTP_SCHEME: String = "http" + const val CAPACITOR_HTTPS_SCHEME: String = "https" + const val CAPACITOR_FILE_START: String = "/_capacitor_file_" + const val CAPACITOR_CONTENT_START: String = "/_capacitor_content_" + const val CAPACITOR_HTTP_INTERCEPTOR_START: String = "/_capacitor_http_interceptor_" + const val CAPACITOR_HTTPS_INTERCEPTOR_START: String = "/_capacitor_https_interceptor_" + + const val DEFAULT_ANDROID_WEBVIEW_VERSION: Int = 60 + const val MINIMUM_ANDROID_WEBVIEW_VERSION: Int = 55 + const val DEFAULT_HUAWEI_WEBVIEW_VERSION: Int = 10 + const val MINIMUM_HUAWEI_WEBVIEW_VERSION: Int = 10 + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java deleted file mode 100644 index c3660265..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.java +++ /dev/null @@ -1,197 +0,0 @@ -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> 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); - setContentView(R.layout.bridge_layout_main); - 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 plugin) { - bridgeBuilder.addPlugin(plugin); - } - - public void registerPlugins(List> 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++; - 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(); - bridge.getApp().fireStatusChange(true); - this.bridge.onResume(); - Logger.debug("App resumed"); - } - - @Override - public void onPause() { - super.onPause(); - this.bridge.onPause(); - Logger.debug("App paused"); - } - - @Override - public void onStop() { - super.onStop(); - - 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(); - 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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.kt new file mode 100644 index 00000000..b85a22d0 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeActivity.kt @@ -0,0 +1,185 @@ +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 kotlin.math.max + +open class BridgeActivity : AppCompatActivity() { + var bridge: Bridge? = null + protected set + protected var keepRunning: Boolean = true + protected var config: CapConfig? = null + + protected var activityDepth: Int = 0 + protected var initialPlugins: List> = ArrayList() + protected val bridgeBuilder: Bridge.Builder = Bridge.Builder( + this + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bridgeBuilder.setInstanceState(savedInstanceState) + application.setTheme(R.style.AppTheme_NoActionBar) + setTheme(R.style.AppTheme_NoActionBar) + setContentView(R.layout.bridge_layout_main) + val loader = PluginManager(assets) + + try { + bridgeBuilder.addPlugins(loader.loadPluginClasses()) + } catch (ex: PluginLoadException) { + Logger.Companion.error("Error loading plugins.", ex) + } + + this.load() + } + + protected fun load() { + Logger.Companion.debug("Starting BridgeActivity") + + bridge = bridgeBuilder.addPlugins(initialPlugins).setConfig(config).create() + + this.keepRunning = bridge!!.shouldKeepRunning() + this.onNewIntent(intent) + } + + fun registerPlugin(plugin: Class?) { + bridgeBuilder.addPlugin(plugin!!) + } + + fun registerPlugins(plugins: List?>?) { + bridgeBuilder.addPlugins(plugins) + } + + public override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + bridge!!.saveInstanceState(outState) + } + + public override fun onStart() { + super.onStart() + activityDepth++ + bridge!!.onStart() + Logger.Companion.debug("App started") + } + + public override fun onRestart() { + super.onRestart() + bridge!!.onRestart() + Logger.Companion.debug("App restarted") + } + + public override fun onResume() { + super.onResume() + bridge!!.app.fireStatusChange(true) + bridge!!.onResume() + Logger.Companion.debug("App resumed") + } + + public override fun onPause() { + super.onPause() + bridge!!.onPause() + Logger.Companion.debug("App paused") + } + + public override fun onStop() { + super.onStop() + + activityDepth = max(0.0, (activityDepth - 1).toDouble()).toInt() + if (activityDepth == 0) { + bridge!!.app.fireStatusChange(false) + } + + bridge!!.onStop() + Logger.Companion.debug("App stopped") + } + + public override fun onDestroy() { + super.onDestroy() + bridge!!.onDestroy() + Logger.Companion.debug("App destroyed") + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + 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 [Bridge.onRequestPermissionsResult] 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 fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + 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 + * [Bridge.onActivityResult] 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 fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (this.bridge == null) { + return + } + + if (!bridge!!.onActivityResult(requestCode, resultCode, data)) { + super.onActivityResult(requestCode, resultCode, data) + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + + if (this.bridge == null || intent == null) { + return + } + + bridge!!.onNewIntent(intent) + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + if (this.bridge == null) { + return + } + + bridge!!.onConfigurationChanged(newConfig) + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.java deleted file mode 100644 index f269bd56..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.java +++ /dev/null @@ -1,134 +0,0 @@ -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> initialPlugins = new ArrayList<>(); - private CapConfig config = null; - - private final List 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 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(); - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.kt new file mode 100644 index 00000000..40a248da --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeFragment.kt @@ -0,0 +1,124 @@ +package com.getcapacitor + +import android.content.Context +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 + +/** + * A simple [Fragment] subclass. + * Use the [BridgeFragment.newInstance] factory method to + * create an instance of this fragment. + */ +class BridgeFragment : Fragment() { + var bridge: Bridge? = null + protected set + protected var keepRunning: Boolean = true + + private val initialPlugins: MutableList> = ArrayList() + private var config: CapConfig? = null + + private val webViewListeners: MutableList = ArrayList() + + fun addPlugin(plugin: Class) { + initialPlugins.add(plugin) + } + + fun setConfig(config: CapConfig?) { + this.config = config + } + + fun addWebViewListener(webViewListener: WebViewListener) { + webViewListeners.add(webViewListener) + } + + /** + * Load the WebView and create the Bridge + */ + protected fun load(savedInstanceState: Bundle?) { + Logger.Companion.debug("Loading Bridge with BridgeFragment") + + val args = arguments + var startDir: String? = null + + if (args != null) { + startDir = arguments!!.getString(ARG_START_DIR) + } + + bridge = + Bridge.Builder(this) + .setInstanceState(savedInstanceState) + .setPlugins(initialPlugins) + .setConfig(config) + .addWebViewListeners(webViewListeners) + .create() + + if (startDir != null) { + bridge!!.setServerAssetPath(startDir) + } + + this.keepRunning = bridge!!.shouldKeepRunning() + } + + override fun onInflate(context: Context, attrs: AttributeSet, savedInstanceState: Bundle?) { + super.onInflate(context, attrs, savedInstanceState) + + val a = context.obtainStyledAttributes(attrs, R.styleable.bridge_fragment) + val c: CharSequence? = a.getString(R.styleable.bridge_fragment_start_dir) + + if (c != null) { + val startDir = c.toString() + val args = Bundle() + args.putString(ARG_START_DIR, startDir) + arguments = args + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + return inflater.inflate(R.layout.fragment_bridge, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + this.load(savedInstanceState) + } + + override fun onDestroy() { + super.onDestroy() + if (this.bridge != null) { + bridge!!.onDestroy() + } + } + + companion object { + private const val ARG_START_DIR = "startDir" + + /** + * 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. + */ + fun newInstance(startDir: String?): BridgeFragment { + val fragment = BridgeFragment() + val args = Bundle() + args.putString(ARG_START_DIR, startDir) + fragment.arguments = args + return fragment + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java deleted file mode 100644 index 400b65a0..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.java +++ /dev/null @@ -1,510 +0,0 @@ -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> permissionCallback = (Map isGranted) -> { - if (permissionListener != null) { - boolean granted = true; - for (Map.Entry 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 onShowCustomView() docs - */ - @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 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 filePathCallback, - final FileChooserParams fileChooserParams - ) { - List 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 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 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 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 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 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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.kt new file mode 100644 index 00000000..894bcb57 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebChromeClient.kt @@ -0,0 +1,534 @@ +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.DialogInterface +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.core.content.FileProvider +import com.getcapacitor.util.PermissionHelper +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Arrays +import java.util.Date + +/** + * Custom WebChromeClient handler, required for showing dialogs, confirms, etc. in our + * WebView instance. + */ +class BridgeWebChromeClient(private val bridge: Bridge) : WebChromeClient() { + private interface PermissionListener { + fun onPermissionSelect(isGranted: Boolean?) + } + + private interface ActivityResultListener { + fun onActivityResult(result: ActivityResult?) + } + + private val permissionLauncher: ActivityResultLauncher<*> + private val activityLauncher: ActivityResultLauncher<*> + private var permissionListener: PermissionListener? = null + private var activityListener: ActivityResultListener? = null + + init { + val permissionCallback = ActivityResultCallback { isGranted: Map -> + if (permissionListener != null) { + var granted = true + for ((_, value) in isGranted) { + if (!value!!) granted = false + } + permissionListener!!.onPermissionSelect(granted) + } + } + + permissionLauncher = + bridge.registerForActivityResult, Map>( + RequestMultiplePermissions(), + permissionCallback + ) + activityLauncher = + bridge.registerForActivityResult( + StartActivityForResult() + ) { result: ActivityResult? -> + if (activityListener != null) { + activityListener!!.onActivityResult(result) + } + } + } + + /** + * Render web content in `view`. + * + * Both this method and [.onHideCustomView] are required for + * rendering web content in full screen. + * + * @see [](https://developer.android.com/reference/android/webkit/WebChromeClient.onShowCustomView + ) */ + override fun onShowCustomView(view: View, callback: CustomViewCallback) { + 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 fun onHideCustomView() { + super.onHideCustomView() + } + + override fun onPermissionRequest(request: PermissionRequest) { + val isRequestPermissionRequired = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + + val permissionList: MutableList = ArrayList() + if (Arrays.asList(*request.resources) + .contains("android.webkit.resource.VIDEO_CAPTURE") + ) { + permissionList.add(Manifest.permission.CAMERA) + } + if (Arrays.asList(*request.resources) + .contains("android.webkit.resource.AUDIO_CAPTURE") + ) { + permissionList.add(Manifest.permission.MODIFY_AUDIO_SETTINGS) + permissionList.add(Manifest.permission.RECORD_AUDIO) + } + if (!permissionList.isEmpty() && isRequestPermissionRequired) { + val permissions = permissionList.toTypedArray() + permissionListener = + PermissionListener { isGranted: Boolean -> + if (isGranted) { + request.grant(request.resources) + } else { + request.deny() + } + } + permissionLauncher.launch(permissions) + } else { + request.grant(request.resources) + } + } + + /** + * Show the browser alert modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsAlert(view: WebView, url: String, message: String, result: JsResult): Boolean { + if (bridge.activity!!.isFinishing) { + return true + } + + val builder = AlertDialog.Builder(view.context) + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, buttonIndex: Int -> + dialog.dismiss() + result.confirm() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + + val dialog = builder.create() + + dialog.show() + + return true + } + + /** + * Show the browser confirm modal + * @param view + * @param url + * @param message + * @param result + * @return + */ + override fun onJsConfirm( + view: WebView, + url: String, + message: String, + result: JsResult + ): Boolean { + if (bridge.activity!!.isFinishing) { + return true + } + + val builder = AlertDialog.Builder(view.context) + + builder + .setMessage(message) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, buttonIndex: Int -> + dialog.dismiss() + result.confirm() + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, buttonIndex: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + + val dialog = builder.create() + + dialog.show() + + return true + } + + /** + * Show the browser prompt modal + * @param view + * @param url + * @param message + * @param defaultValue + * @param result + * @return + */ + override fun onJsPrompt( + view: WebView, + url: String, + message: String, + defaultValue: String, + result: JsPromptResult + ): Boolean { + if (bridge.activity!!.isFinishing) { + return true + } + + val builder = AlertDialog.Builder(view.context) + val input = EditText(view.context) + + builder + .setMessage(message) + .setView(input) + .setPositiveButton( + "OK" + ) { dialog: DialogInterface, buttonIndex: Int -> + dialog.dismiss() + val inputText1 = input.text.toString().trim { it <= ' ' } + result.confirm(inputText1) + } + .setNegativeButton( + "Cancel" + ) { dialog: DialogInterface, buttonIndex: Int -> + dialog.dismiss() + result.cancel() + } + .setOnCancelListener { dialog: DialogInterface -> + dialog.dismiss() + result.cancel() + } + + val dialog = builder.create() + + dialog.show() + + return true + } + + /** + * Handle the browser geolocation permission prompt + * @param origin + * @param callback + */ + override fun onGeolocationPermissionsShowPrompt( + origin: String, + callback: GeolocationPermissions.Callback + ) { + super.onGeolocationPermissionsShowPrompt(origin, callback) + Logger.Companion.debug("onGeolocationPermissionsShowPrompt: DOING IT HERE FOR ORIGIN: $origin") + val geoPermissions = arrayOf( + Manifest.permission.ACCESS_COARSE_LOCATION, + Manifest.permission.ACCESS_FINE_LOCATION + ) + + if (!PermissionHelper.hasPermissions(bridge.getContext(), geoPermissions)) { + permissionListener = + PermissionListener { isGranted: Boolean -> + if (isGranted) { + callback.invoke(origin, true, false) + } else { + val coarsePermission = arrayOf(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.Companion.debug("onGeolocationPermissionsShowPrompt: has required permission") + } + } + + override fun onShowFileChooser( + webView: WebView, + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams + ): Boolean { + val acceptTypes = Arrays.asList(*fileChooserParams.acceptTypes) + val captureEnabled = fileChooserParams.isCaptureEnabled + val capturePhoto = captureEnabled && acceptTypes.contains("image/*") + val captureVideo = captureEnabled && acceptTypes.contains("video/*") + if ((capturePhoto || captureVideo)) { + if (isMediaCaptureSupported) { + showMediaCaptureOrFilePicker(filePathCallback, fileChooserParams, captureVideo) + } else { + permissionListener = + PermissionListener { isGranted: Boolean -> + if (isGranted) { + showMediaCaptureOrFilePicker( + filePathCallback, + fileChooserParams, + captureVideo + ) + } else { + Logger.Companion.warn( + Logger.Companion.tags("FileChooser"), + "Camera permission not granted" + ) + filePathCallback.onReceiveValue(null) + } + } + val camPermission = arrayOf(Manifest.permission.CAMERA) + permissionLauncher.launch(camPermission) + } + } else { + showFilePicker(filePathCallback, fileChooserParams) + } + + return true + } + + private val isMediaCaptureSupported: Boolean + get() { + val permissions = arrayOf(Manifest.permission.CAMERA) + return (PermissionHelper.hasPermissions(bridge.getContext(), permissions) || + !PermissionHelper.hasDefinedPermission( + bridge.getContext(), + Manifest.permission.CAMERA + ) + ) + } + + private fun showMediaCaptureOrFilePicker( + filePathCallback: ValueCallback>, + fileChooserParams: FileChooserParams, + isVideo: Boolean + ) { + // 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 + val isVideoCaptureSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + var shown = false + shown = if (isVideo && isVideoCaptureSupported) { + showVideoCapturePicker(filePathCallback) + } else { + showImageCapturePicker(filePathCallback) + } + if (!shown) { + Logger.Companion.warn( + Logger.Companion.tags("FileChooser"), + "Media capture intent could not be launched. Falling back to default file picker." + ) + showFilePicker(filePathCallback, fileChooserParams) + } + } + + @SuppressLint("QueryPermissionsNeeded") + private fun showImageCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + if (takePictureIntent.resolveActivity(bridge.activity!!.packageManager) == null) { + return false + } + + val imageFileUri: Uri + try { + imageFileUri = createImageFileUri() + } catch (ex: Exception) { + Logger.Companion.error("Unable to create temporary media capture file: " + ex.message) + return false + } + takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageFileUri) + activityListener = + ActivityResultListener { activityResult: ActivityResult -> + var result: Array? = null + if (activityResult.resultCode == Activity.RESULT_OK) { + result = arrayOf(imageFileUri) + } + filePathCallback.onReceiveValue(result) + } + activityLauncher.launch(takePictureIntent) + + return true + } + + @SuppressLint("QueryPermissionsNeeded") + private fun showVideoCapturePicker(filePathCallback: ValueCallback?>): Boolean { + val takeVideoIntent = Intent(MediaStore.ACTION_VIDEO_CAPTURE) + if (takeVideoIntent.resolveActivity(bridge.activity!!.packageManager) == null) { + return false + } + + activityListener = + ActivityResultListener { activityResult: ActivityResult -> + var result: Array? = null + if (activityResult.resultCode == Activity.RESULT_OK) { + result = arrayOf(activityResult.data!!.data) + } + filePathCallback.onReceiveValue(result) + } + activityLauncher.launch(takeVideoIntent) + + return true + } + + private fun showFilePicker( + filePathCallback: ValueCallback?>, + fileChooserParams: FileChooserParams + ) { + val intent = fileChooserParams.createIntent() + if (fileChooserParams.mode == FileChooserParams.MODE_OPEN_MULTIPLE) { + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + } + if (fileChooserParams.acceptTypes.size > 1 || intent.type!!.startsWith(".")) { + val validTypes = getValidTypes(fileChooserParams.acceptTypes) + intent.putExtra(Intent.EXTRA_MIME_TYPES, validTypes) + if (intent.type!!.startsWith(".")) { + intent.setType(validTypes[0]) + } + } + try { + activityListener = + ActivityResultListener { activityResult: ActivityResult -> + val result: Array? + val resultIntent = activityResult.data + if (activityResult.resultCode == Activity.RESULT_OK && resultIntent!!.clipData != null) { + val numFiles = resultIntent.clipData!!.itemCount + result = arrayOfNulls(numFiles) + for (i in 0 until numFiles) { + result[i] = resultIntent.clipData!!.getItemAt(i).uri + } + } else { + result = + FileChooserParams.parseResult(activityResult.resultCode, resultIntent) + } + filePathCallback.onReceiveValue(result) + } + activityLauncher.launch(intent) + } catch (e: ActivityNotFoundException) { + filePathCallback.onReceiveValue(null) + } + } + + private fun getValidTypes(currentTypes: Array): Array { + val validTypes: MutableList = ArrayList() + val mtm = MimeTypeMap.getSingleton() + for (mime in currentTypes) { + if (mime.startsWith(".")) { + val extension = mime.substring(1) + val extensionMime = mtm.getMimeTypeFromExtension(extension) + if (extensionMime != null && !validTypes.contains(extensionMime)) { + validTypes.add(extensionMime) + } + } else if (!validTypes.contains(mime)) { + validTypes.add(mime) + } + } + val validObj: Array = validTypes.toTypedArray() + return Arrays.copyOf(validObj, validObj.size, Array::class.java) + } + + override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean { + val tag: String = Logger.Companion.tags("Console") + if (consoleMessage.message() != null && isValidMsg(consoleMessage.message())) { + val msg = String.format( + "File: %s - Line %d - Msg: %s", + consoleMessage.sourceId(), + consoleMessage.lineNumber(), + consoleMessage.message() + ) + val level = consoleMessage.messageLevel().name + if ("ERROR".equals(level, ignoreCase = true)) { + Logger.Companion.error(tag, msg, null) + } else if ("WARNING".equals(level, ignoreCase = true)) { + Logger.Companion.warn(tag, msg) + } else if ("TIP".equals(level, ignoreCase = true)) { + Logger.Companion.debug(tag, msg) + } else { + Logger.Companion.info(tag, msg) + } + } + return true + } + + fun isValidMsg(msg: String): Boolean { + return !(msg.contains("%cresult %c") || + (msg.contains("%cnative %c")) || + msg.equals("[object Object]", ignoreCase = true) || + msg.equals("console.groupEnd", ignoreCase = true) + ) + } + + @Throws(IOException::class) + private fun createImageFileUri(): Uri { + val activity: Activity? = bridge.activity + val photoFile = createImageFile(activity) + return FileProvider.getUriForFile( + activity!!, + bridge.getContext()!!.packageName + ".fileprovider", + photoFile + ) + } + + @Throws(IOException::class) + private fun createImageFile(activity: Activity?): File { + // Create an image file name + val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date()) + val imageFileName = "JPEG_" + timeStamp + "_" + val storageDir = activity!!.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + + return File.createTempFile(imageFileName, ".jpg", storageDir) + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java deleted file mode 100644 index c434247a..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.java +++ /dev/null @@ -1,111 +0,0 @@ -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 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 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 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 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 webViewListeners = bridge.getWebViewListeners(); - if (webViewListeners != null) { - for (WebViewListener listener : bridge.getWebViewListeners()) { - result = listener.onRenderProcessGone(view, detail) || result; - } - } - - return result; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.kt new file mode 100644 index 00000000..bfec7991 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/BridgeWebViewClient.kt @@ -0,0 +1,106 @@ +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 + +open class BridgeWebViewClient(private val bridge: Bridge) : WebViewClient() { + override fun shouldInterceptRequest( + view: WebView, + request: WebResourceRequest + ): WebResourceResponse? { + return bridge.localServer!!.shouldInterceptRequest(request) + } + + override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { + val url = request.url + return bridge.launchIntent(url) + } + + @Deprecated("") + override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { + return bridge.launchIntent(Uri.parse(url)) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + val webViewListeners = bridge.getWebViewListeners() + + if (webViewListeners != null && view.progress == 100) { + for (listener in bridge.getWebViewListeners()) { + listener.onPageLoaded(view) + } + } + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + super.onReceivedError(view, request, error) + + val webViewListeners = bridge.getWebViewListeners() + if (webViewListeners != null) { + for (listener in bridge.getWebViewListeners()) { + listener.onReceivedError(view) + } + } + + val errorPath = bridge.errorUrl + if (errorPath != null && request.isForMainFrame) { + view.loadUrl(errorPath) + } + } + + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap) { + super.onPageStarted(view, url, favicon) + bridge.reset() + val webViewListeners = bridge.getWebViewListeners() + + if (webViewListeners != null) { + for (listener in bridge.getWebViewListeners()) { + listener.onPageStarted(view) + } + } + } + + override fun onReceivedHttpError( + view: WebView, + request: WebResourceRequest, + errorResponse: WebResourceResponse + ) { + super.onReceivedHttpError(view, request, errorResponse) + + val webViewListeners = bridge.getWebViewListeners() + if (webViewListeners != null) { + for (listener in bridge.getWebViewListeners()) { + listener.onReceivedHttpError(view) + } + } + + val errorPath = bridge.errorUrl + if (errorPath != null && request.isForMainFrame) { + view.loadUrl(errorPath) + } + } + + override fun onRenderProcessGone(view: WebView, detail: RenderProcessGoneDetail): Boolean { + super.onRenderProcessGone(view, detail) + var result = false + + val webViewListeners = bridge.getWebViewListeners() + if (webViewListeners != null) { + for (listener in bridge.getWebViewListeners()) { + result = listener.onRenderProcessGone(view, detail) || result + } + } + + return result + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java deleted file mode 100644 index 63db2a47..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.java +++ /dev/null @@ -1,670 +0,0 @@ -package com.getcapacitor; - -import static com.getcapacitor.Bridge.CAPACITOR_HTTP_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_HTTP_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; - - // Embedded - private String startPath; - - // Plugins - private Map 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; - - // 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); - - 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 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 http."); - 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 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 deserializePluginsConfig(JSONObject pluginsConfig) { - Map pluginsMap = new HashMap<>(); - - // return an empty map if there is no pluginsConfig json - if (pluginsConfig == null) { - return pluginsMap; - } - - Iterator 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_HTTP_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; - - // Embedded - private String startPath = null; - - // Plugins Config Object - private Map 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 setLoggingEnabled(boolean enabled) { - this.loggingEnabled = enabled; - return this; - } - - public Builder setInitialFocus(boolean focus) { - this.initialFocus = focus; - return this; - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.kt new file mode 100644 index 00000000..a21cedfc --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapConfig.kt @@ -0,0 +1,650 @@ +package com.getcapacitor + +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.res.AssetManager +import com.getcapacitor.util.JSONUtils +import org.json.JSONException +import org.json.JSONObject +import java.io.File +import java.io.IOException + +/** + * Represents the configuration options for Capacitor + */ +class CapConfig { + // Server Config + var isHTML5Mode: Boolean = true + private set + var serverUrl: String? = null + private set + var hostname: String = "localhost" + private set + var androidScheme: String = Bridge.CAPACITOR_HTTP_SCHEME + private set + var allowNavigation: Array + private set + + // Android Config + var overriddenUserAgentString: String? = null + private set + var appendedUserAgentString: String? = null + private set + var backgroundColor: String? = null + private set + var isMixedContentAllowed: Boolean = false + private set + var isInputCaptured: Boolean = false + private set + var isWebContentsDebuggingEnabled: Boolean = false + private set + var isLoggingEnabled: Boolean = true + private set + var isInitialFocus: Boolean = true + private set + var isUsingLegacyBridge: Boolean = false + private set + private var minWebViewVersion = Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION + private var minHuaweiWebViewVersion = Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION + var errorPath: String? = null + private set + + // Embedded + var startPath: String? = null + private set + + // Plugins + private var pluginsConfiguration: Map? = null + + // Config Object JSON (legacy) + private var configJSON = JSONObject() + + /** + * Constructs an empty config file. + */ + private constructor() + + /** + * Get an instance of the Config file object. + * @param assetManager The AssetManager used to load the config file + * @param config JSON describing a configuration to use + */ + @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. + + """ + ) + constructor(assetManager: AssetManager, config: JSONObject?) { + if (config != null) { + this.configJSON = config + } else { + // Load the capacitor.config.json + loadConfigFromAssets(assetManager, null) + } + + deserializeConfig(null) + } + + /** + * Constructs a Capacitor Configuration using ConfigBuilder. + * + * @param builder A config builder initialized with values + */ + private constructor(builder: Builder) { + // Server Config + this.isHTML5Mode = 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.isMixedContentAllowed = builder.allowMixedContent + this.isInputCaptured = builder.captureInput + this.isWebContentsDebuggingEnabled = builder.webContentsDebuggingEnabled!! + this.isLoggingEnabled = builder.loggingEnabled + this.isInitialFocus = builder.initialFocus + this.isUsingLegacyBridge = builder.useLegacyBridge + this.minWebViewVersion = builder.minWebViewVersion + this.minHuaweiWebViewVersion = builder.minHuaweiWebViewVersion + this.errorPath = builder.errorPath + + // 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 fun loadConfigFromAssets(assetManager: AssetManager, path: String?) { + var path = 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[path.length - 1] != '/') { + path = "$path/" + } + } + + try { + val jsonString = + FileUtils.readFileFromAssets(assetManager, path + "capacitor.config.json") + configJSON = JSONObject(jsonString) + } catch (ex: IOException) { + Logger.Companion.error( + "Unable to load capacitor.config.json. Run npx cap copy first", + ex + ) + } catch (ex: JSONException) { + Logger.Companion.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 fun loadConfigFromFile(path: String) { + var path: 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[path.length - 1] != '/') { + path = "$path/" + } + } + + try { + val configFile = File(path + "capacitor.config.json") + val jsonString = FileUtils.readFileFromDisk(configFile) + configJSON = JSONObject(jsonString) + } catch (ex: JSONException) { + Logger.Companion.error( + "Unable to parse capacitor.config.json. Make sure it's valid json", + ex + ) + } catch (ex: IOException) { + Logger.Companion.error("Unable to load capacitor.config.json.", ex) + } + } + + /** + * Deserializes the config from JSON into a Capacitor Configuration object. + */ + private fun deserializeConfig(context: Context?) { + val isDebug = + context != null && (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + + // Server + isHTML5Mode = JSONUtils.getBoolean(configJSON, "server.html5mode", isHTML5Mode) + serverUrl = JSONUtils.getString(configJSON, "server.url", null) + hostname = JSONUtils.getString(configJSON, "server.hostname", hostname) + errorPath = JSONUtils.getString(configJSON, "server.errorPath", null) + + val 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) + ) + isMixedContentAllowed = + JSONUtils.getBoolean( + configJSON, + "android.allowMixedContent", + JSONUtils.getBoolean(configJSON, "allowMixedContent", isMixedContentAllowed) + ) + minWebViewVersion = JSONUtils.getInt( + configJSON, + "android.minWebViewVersion", + Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION + ) + minHuaweiWebViewVersion = JSONUtils.getInt( + configJSON, + "android.minHuaweiWebViewVersion", + Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION + ) + isInputCaptured = JSONUtils.getBoolean(configJSON, "android.captureInput", isInputCaptured) + isUsingLegacyBridge = + JSONUtils.getBoolean(configJSON, "android.useLegacyBridge", isUsingLegacyBridge) + isWebContentsDebuggingEnabled = + JSONUtils.getBoolean(configJSON, "android.webContentsDebuggingEnabled", isDebug) + + val logBehavior = JSONUtils.getString( + configJSON, + "android.loggingBehavior", + JSONUtils.getString(configJSON, "loggingBehavior", LOG_BEHAVIOR_DEBUG) + ) + when (logBehavior.lowercase()) { + LOG_BEHAVIOR_PRODUCTION -> isLoggingEnabled = true + LOG_BEHAVIOR_NONE -> isLoggingEnabled = false + else -> isLoggingEnabled = isDebug + } + isInitialFocus = JSONUtils.getBoolean(configJSON, "android.initialFocus", isInitialFocus) + + // Plugins + pluginsConfiguration = deserializePluginsConfig(JSONUtils.getObject(configJSON, "plugins")) + } + + private fun validateScheme(scheme: String): Boolean { + val invalidSchemes: List = + mutableListOf("file", "ftp", "ftps", "ws", "wss", "about", "blob", "data") + if (invalidSchemes.contains(scheme)) { + Logger.Companion.warn("$scheme is not an allowed scheme. Defaulting to http.") + return false + } + + // Non-http(s) schemes are not allowed to modify the URL path as of Android Webview 117 + if (scheme != "http" && scheme != "https") { + Logger.Companion.warn( + "Using a non-standard scheme: $scheme for Android. This is known to cause issues as of Android Webview 117." + ) + } + + return true + } + + fun getMinWebViewVersion(): Int { + if (minWebViewVersion < Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION) { + Logger.Companion.warn("Specified minimum webview version is too low, defaulting to " + Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION) + return Bridge.MINIMUM_ANDROID_WEBVIEW_VERSION + } + + return minWebViewVersion + } + + fun getMinHuaweiWebViewVersion(): Int { + if (minHuaweiWebViewVersion < Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION) { + Logger.Companion.warn("Specified minimum Huawei webview version is too low, defaulting to " + Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION) + return Bridge.MINIMUM_HUAWEI_WEBVIEW_VERSION + } + + return minHuaweiWebViewVersion + } + + fun getPluginConfiguration(pluginId: String?): PluginConfig { + var pluginConfig = pluginsConfiguration!![pluginId] + if (pluginConfig == null) { + pluginConfig = PluginConfig(JSONObject()) + } + + return pluginConfig + } + + /** + * Get a JSON object value from the Capacitor config. + * @param key A key to fetch from the config + * @return The value from the config, if exists. Null if not + */ + @Deprecated( + """use {@link PluginConfig#getObject(String)} to access plugin config values. + For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getObject(key: String?): JSONObject? { + try { + return configJSON.getJSONObject(key) + } catch (ex: Exception) { + } + return null + } + + /** + * Get a string value from the Capacitor config. + * @param key A key to fetch from the config + * @return The value from the config, if exists. Null if not + */ + @Deprecated( + """use {@link PluginConfig#getString(String, String)} to access plugin config + values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getString(key: String?): String { + return JSONUtils.getString(configJSON, key, null) + } + + /** + * Get a string value from the Capacitor config. + * @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( + """use {@link PluginConfig#getString(String, String)} to access plugin config + values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getString(key: String?, defaultValue: String?): String { + return JSONUtils.getString(configJSON, key, defaultValue) + } + + /** + * Get a boolean value from the Capacitor config. + * @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( + """use {@link PluginConfig#getBoolean(String, boolean)} to access plugin config + values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getBoolean(key: String?, defaultValue: Boolean): Boolean { + return JSONUtils.getBoolean(configJSON, key, defaultValue) + } + + /** + * Get an integer value from the Capacitor config. + * @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( + """use {@link PluginConfig#getInt(String, int)} to access the plugin config + values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getInt(key: String?, defaultValue: Int): Int { + return JSONUtils.getInt(configJSON, key, defaultValue) + } + + /** + * Get a string array value from the Capacitor config. + * @param key A key to fetch from the config + * @return The value from the config, if exists. Null if not + */ + @Deprecated( + """use {@link PluginConfig#getArray(String)} to access the plugin config + values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getArray(key: String?): Array { + return JSONUtils.getArray(configJSON, key, null) + } + + /** + * Get a string array value from the Capacitor config. + * @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( + """use {@link PluginConfig#getArray(String, String[])} to access the plugin + config values. For main Capacitor config values, use the appropriate getter. + + """ + ) + fun getArray(key: String?, defaultValue: Array?): Array { + return JSONUtils.getArray(configJSON, key, defaultValue) + } + + /** + * Builds a Capacitor Configuration in code + */ + class Builder + /** + * Constructs a new CapConfig Builder. + * + * @param context The context + */(private val context: Context) { + // Server Config Values + var html5mode: Boolean = true + var serverUrl: String? = null + var errorPath: String? = null + var hostname: String = "localhost" + var androidScheme: String = Bridge.CAPACITOR_HTTP_SCHEME + var allowNavigation: Array + + // Android Config Values + var overriddenUserAgentString: String? = null + var appendedUserAgentString: String? = null + var backgroundColor: String? = null + var allowMixedContent: Boolean = false + var captureInput: Boolean = false + var webContentsDebuggingEnabled: Boolean? = null + var loggingEnabled: Boolean = true + var initialFocus: Boolean = false + var useLegacyBridge: Boolean = false + val minWebViewVersion: Int = Bridge.DEFAULT_ANDROID_WEBVIEW_VERSION + val minHuaweiWebViewVersion: Int = Bridge.DEFAULT_HUAWEI_WEBVIEW_VERSION + + // Embedded + var startPath: String? = null + + // Plugins Config Object + var pluginsConfiguration: Map = HashMap() + + /** + * Builds a Capacitor Config from the builder. + * + * @return A new Capacitor Config + */ + fun create(): CapConfig { + if (webContentsDebuggingEnabled == null) { + webContentsDebuggingEnabled = + (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0 + } + + return CapConfig(this) + } + + fun setPluginsConfiguration(pluginsConfiguration: JSONObject?): Builder { + this.pluginsConfiguration = deserializePluginsConfig(pluginsConfiguration) + return this + } + + fun setHTML5mode(html5mode: Boolean): Builder { + this.html5mode = html5mode + return this + } + + fun setServerUrl(serverUrl: String?): Builder { + this.serverUrl = serverUrl + return this + } + + fun setErrorPath(errorPath: String?): Builder { + this.errorPath = errorPath + return this + } + + fun setHostname(hostname: String): Builder { + this.hostname = hostname + return this + } + + fun setStartPath(path: String?): Builder { + this.startPath = path + return this + } + + fun setAndroidScheme(androidScheme: String): Builder { + this.androidScheme = androidScheme + return this + } + + fun setAllowNavigation(allowNavigation: Array): Builder { + this.allowNavigation = allowNavigation + return this + } + + fun setOverriddenUserAgentString(overriddenUserAgentString: String?): Builder { + this.overriddenUserAgentString = overriddenUserAgentString + return this + } + + fun setAppendedUserAgentString(appendedUserAgentString: String?): Builder { + this.appendedUserAgentString = appendedUserAgentString + return this + } + + fun setBackgroundColor(backgroundColor: String?): Builder { + this.backgroundColor = backgroundColor + return this + } + + fun setAllowMixedContent(allowMixedContent: Boolean): Builder { + this.allowMixedContent = allowMixedContent + return this + } + + fun setCaptureInput(captureInput: Boolean): Builder { + this.captureInput = captureInput + return this + } + + fun setUseLegacyBridge(useLegacyBridge: Boolean): Builder { + this.useLegacyBridge = useLegacyBridge + return this + } + + fun setWebContentsDebuggingEnabled(webContentsDebuggingEnabled: Boolean): Builder { + this.webContentsDebuggingEnabled = webContentsDebuggingEnabled + return this + } + + fun setLoggingEnabled(enabled: Boolean): Builder { + this.loggingEnabled = enabled + return this + } + + fun setInitialFocus(focus: Boolean): Builder { + this.initialFocus = focus + return this + } + } + + companion object { + private const val LOG_BEHAVIOR_NONE = "none" + private const val LOG_BEHAVIOR_DEBUG = "debug" + private const val LOG_BEHAVIOR_PRODUCTION = "production" + + /** + * Constructs a Capacitor Configuration from config.json file. + * + * @param context The context. + * @return A loaded config file, if successful. + */ + fun loadDefault(context: Context?): CapConfig { + val config = CapConfig() + + if (context == null) { + Logger.Companion.error("Capacitor Config could not be created from file. Context must not be null.") + return config + } + + config.loadConfigFromAssets(context.assets, 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. + */ + fun loadFromAssets(context: Context?, path: String?): CapConfig { + val config = CapConfig() + + if (context == null) { + Logger.Companion.error("Capacitor Config could not be created from file. Context must not be null.") + return config + } + + config.loadConfigFromAssets(context.assets, 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. + */ + fun loadFromFile(context: Context?, path: String): CapConfig { + val config = CapConfig() + + if (context == null) { + Logger.Companion.error("Capacitor Config could not be created from file. Context must not be null.") + return config + } + + config.loadConfigFromFile(path) + config.deserializeConfig(context) + return config + } + + private fun deserializePluginsConfig(pluginsConfig: JSONObject?): Map { + val pluginsMap: MutableMap = HashMap() + + // return an empty map if there is no pluginsConfig json + if (pluginsConfig == null) { + return pluginsMap + } + + val pluginIds = pluginsConfig.keys() + + while (pluginIds.hasNext()) { + val pluginId = pluginIds.next() + var value: JSONObject? = null + + try { + value = pluginsConfig.getJSONObject(pluginId) + val pluginConfig = PluginConfig(value) + pluginsMap[pluginId] = pluginConfig + } catch (e: JSONException) { + e.printStackTrace() + } + } + + return pluginsMap + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java deleted file mode 100644 index e46b904a..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.java +++ /dev/null @@ -1,52 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.kt new file mode 100644 index 00000000..51be2e46 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/CapacitorWebView.kt @@ -0,0 +1,49 @@ +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 + +class CapacitorWebView(context: Context?, attrs: AttributeSet?) : WebView( + context!!, attrs +) { + private var capInputConnection: BaseInputConnection? = null + private var bridge: Bridge? = null + + fun setBridge(bridge: Bridge?) { + this.bridge = bridge + } + + override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection { + val config = if (bridge != null) { + bridge!!.config + } else { + CapConfig.Companion.loadDefault(context) + } + + val captureInput = config.isInputCaptured + if (captureInput) { + if (capInputConnection == null) { + capInputConnection = BaseInputConnection(this, false) + } + return capInputConnection!! + } + return super.onCreateInputConnection(outAttrs) + } + + @Suppress("deprecation") + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_MULTIPLE) { + evaluateJavascript( + "document.activeElement.value = document.activeElement.value + '" + event.characters + "';", + null + ) + return false + } + return super.dispatchKeyEvent(event) + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java deleted file mode 100644 index 47add8cd..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.java +++ /dev/null @@ -1,292 +0,0 @@ -/** - * 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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.kt new file mode 100644 index 00000000..4f29e3f8 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/FileUtils.kt @@ -0,0 +1,287 @@ +/** + * 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.InputStreamReader +import kotlin.math.min + +/** + * Common File utilities, such as resolve content URIs and + * creating portable web paths from low-level files + */ +object FileUtils { + private const val CapacitorFileScheme = Bridge.CAPACITOR_FILE_START + + fun getPortablePath(c: Context, host: String, u: Uri): String { + var path = getFileUrlForUri(c, u) + if (path!!.startsWith("file://")) { + path = path.replace("file://", "") + } + return host + Bridge.CAPACITOR_FILE_START + path + } + + fun getFileUrlForUri(context: Context, uri: Uri): String? { + // DocumentProvider + if (DocumentsContract.isDocumentUri(context, uri)) { + // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + + if ("primary".equals(type, ignoreCase = true)) { + return legacyPrimaryPath(split[1]) + } else { + val splitIndex = docId.indexOf(':', 1) + val tag = docId.substring(0, splitIndex) + val path = docId.substring(splitIndex + 1) + + val nonPrimaryVolume = getPathToNonPrimaryVolume(context, tag) + if (nonPrimaryVolume != null) { + val result = "$nonPrimaryVolume/$path" + val file = File(result) + if (file.exists() && file.canRead()) { + return result + } + return null + } + } + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + id.toLong() + ) + + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val type = split[0] + + var contentUri: Uri? = null + if ("image" == type) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else if ("video" == type) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } else if ("audio" == type) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if ("content".equals(uri.scheme, ignoreCase = true)) { + // Return the remote address + if (isGooglePhotosUri(uri)) return uri.lastPathSegment + return getDataColumn(context, uri, null, null) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + + return null + } + + @Suppress("deprecation") + private fun legacyPrimaryPath(pathPart: String): String { + return Environment.getExternalStorageDirectory().toString() + "/" + 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. + */ + @Throws(IOException::class) + fun readFileFromAssets(assetManager: AssetManager, fileName: String?): String { + BufferedReader(InputStreamReader(assetManager.open(fileName!!))).use { reader -> + val buffer = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != 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. + */ + @Throws(IOException::class) + fun readFileFromDisk(file: File?): String { + BufferedReader(FileReader(file)).use { reader -> + val buffer = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != 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 fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): String? { + var path: String? = null + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf(column) + + try { + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + path = cursor.getString(index) + } + } catch (ex: IllegalArgumentException) { + return getCopyFilePath(uri, context) + } finally { + cursor?.close() + } + if (path == null) { + return getCopyFilePath(uri, context) + } + return path + } + + private fun getCopyFilePath(uri: Uri?, context: Context): String? { + val cursor = context.contentResolver.query(uri!!, null, null, null, null) + val nameIndex = cursor!!.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + val name = (cursor.getString(nameIndex)) + val file = File(context.filesDir, name) + try { + val inputStream = context.contentResolver.openInputStream(uri) + val outputStream = FileOutputStream(file) + var read = 0 + val maxBufferSize = 1024 * 1024 + val bufferSize = min(inputStream!!.available().toDouble(), maxBufferSize.toDouble()) + .toInt() + val buffers = ByteArray(bufferSize) + while ((inputStream.read(buffers).also { read = it }) != -1) { + outputStream.write(buffers, 0, read) + } + inputStream.close() + outputStream.close() + } catch (e: Exception) { + return null + } finally { + if (cursor != null) cursor.close() + } + return file.path + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } + + private fun getPathToNonPrimaryVolume(context: Context, tag: String): String? { + val volumes = context.externalCacheDirs + if (volumes != null) { + for (volume in volumes) { + if (volume != null) { + val path = volume.absolutePath + if (path != null) { + val index = path.indexOf(tag) + if (index != -1) { + return path.substring(0, index) + tag + } + } + } + } + } + return null + } + + enum class Type(private val type: String) { + IMAGE("image") + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java deleted file mode 100644 index 1757e326..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.getcapacitor; - -class InvalidPluginException extends Exception { - - public InvalidPluginException(String s) { - super(s); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.kt new file mode 100644 index 00000000..531f54a0 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginException.kt @@ -0,0 +1,3 @@ +package com.getcapacitor + +internal class InvalidPluginException(s: String?) : Exception(s) diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java deleted file mode 100644 index 94be491e..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.java +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.kt new file mode 100644 index 00000000..c552b811 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/InvalidPluginMethodException.kt @@ -0,0 +1,9 @@ +package com.getcapacitor + +internal class InvalidPluginMethodException : Exception { + constructor(s: String?) : super(s) + + constructor(t: Throwable?) : super(t) + + constructor(s: String?, t: Throwable?) : super(s, t) +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.java deleted file mode 100644 index 06b7f4dd..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.java +++ /dev/null @@ -1,51 +0,0 @@ -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 List toList() throws JSONException { - List 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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.kt new file mode 100644 index 00000000..c08fb5aa --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSArray.kt @@ -0,0 +1,42 @@ +package com.getcapacitor + +import org.json.JSONArray +import org.json.JSONException + +class JSArray : JSONArray { + constructor() : super() + + constructor(json: String?) : super(json) + + constructor(copyFrom: Collection<*>?) : super(copyFrom) + + constructor(array: Any?) : super(array) + + @Throws(JSONException::class) + fun toList(): List { + val items: MutableList = ArrayList() + var o: Any? = null + for (i in 0 until this.length()) { + o = this[i] + try { + items.add(this[i] as E) + } catch (ex: Exception) { + throw JSONException("Not all items are instances of the given type") + } + } + return items + } + + companion object { + /** + * Create a new JSArray without throwing a error + */ + fun from(array: Any?): JSArray? { + try { + return JSArray(array) + } catch (ex: JSONException) { + } + return null + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.java deleted file mode 100644 index 382f4b5d..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.java +++ /dev/null @@ -1,193 +0,0 @@ -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 plugins) { - List 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 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 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 lines = new ArrayList<>(); - - List 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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.kt new file mode 100644 index 00000000..47bbb2de --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExport.kt @@ -0,0 +1,183 @@ +package com.getcapacitor + +import android.content.Context +import android.text.TextUtils +import com.getcapacitor.JSExportException +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException + +object JSExport { + private const val CATCHALL_OPTIONS_PARAM = "_options" + private const val CALLBACK_PARAM = "_callback" + + fun getGlobalJS(context: Context?, loggingEnabled: Boolean, isDebug: Boolean): String { + return "window.Capacitor = { DEBUG: $isDebug, isLoggingEnabled: $loggingEnabled, Plugins: {} };" + } + + fun getCordovaJS(context: Context): String? { + var fileContent: String? = "" + try { + fileContent = FileUtils.readFileFromAssets(context.assets, "public/cordova.js") + } catch (ex: IOException) { + Logger.Companion.error("Unable to read public/cordova.js file, Cordova plugins will not work") + } + return fileContent + } + + fun getCordovaPluginsFileJS(context: Context): String? { + var fileContent: String? = "" + try { + fileContent = FileUtils.readFileFromAssets(context.assets, "public/cordova_plugins.js") + } catch (ex: IOException) { + Logger.Companion.error("Unable to read public/cordova_plugins.js file, Cordova plugins will not work") + } + return fileContent + } + + fun getPluginJS(plugins: Collection): String { + val lines: MutableList = ArrayList() + val pluginArray = JSONArray() + + lines.add("// Begin: Capacitor Plugin JS") + for (plugin in plugins) { + lines.add( + """(function(w) { +var a = (w.Capacitor = w.Capacitor || {}); +var p = (a.Plugins = a.Plugins || {}); +var t = (p['${plugin.id}'] = {}); +t.addListener = function(eventName, callback) { + return w.Capacitor.addListener('${plugin.id}', eventName, callback); +}""" + ) + val methods = plugin.methods + for (method in methods!!) { + if (method.name == "addListener" || method.name == "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)} + window.Capacitor.PluginHeaders = $pluginArray; + """.trimIndent() + } + + fun getCordovaPluginJS(context: Context): String? { + return getFilesContent(context, "public/plugins") + } + + fun getFilesContent(context: Context, path: String): String? { + val builder = StringBuilder() + try { + val content = context.assets.list(path) + if (content!!.size > 0) { + for (file in content) { + if (!file.endsWith(".map")) { + builder.append(getFilesContent(context, "$path/$file")) + } + } + } else { + return FileUtils.readFileFromAssets(context.assets, path) + } + } catch (ex: IOException) { + Logger.Companion.warn("Unable to read file at path $path") + } + return builder.toString() + } + + private fun createPluginHeader(plugin: PluginHandle): JSONObject { + val pluginObj = JSONObject() + val methods = plugin.methods + try { + val id = plugin.id + val methodArray = JSONArray() + pluginObj.put("name", id) + + for (method in methods!!) { + methodArray.put(createPluginMethodHeader(method)) + } + + pluginObj.put("methods", methodArray) + } catch (e: JSONException) { + // ignore + } + return pluginObj + } + + private fun createPluginMethodHeader(method: PluginMethodHandle?): JSONObject { + val methodObj = JSONObject() + + try { + methodObj.put("name", method.getName()) + if (method.getReturnType() != PluginMethod.Companion.RETURN_NONE) { + methodObj.put("rtype", method.getReturnType()) + } + } catch (e: JSONException) { + // ignore + } + + return methodObj + } + + @Throws(JSExportException::class) + fun getBridgeJS(context: Context): String? { + return getFilesContent(context, "native-bridge.js") + } + + private fun generateMethodJS(plugin: PluginHandle, method: PluginMethodHandle?): String { + val lines: MutableList = ArrayList() + + val args: MutableList = ArrayList() + // Add the catch all param that will take a full javascript object to pass to the plugin + args.add(CATCHALL_OPTIONS_PARAM) + + val returnType = method.getReturnType() + if (returnType == PluginMethod.Companion.RETURN_CALLBACK) { + args.add(CALLBACK_PARAM) + } + + // Create the method function declaration + lines.add("t['" + method.getName() + "'] = function(" + TextUtils.join(", ", args) + ") {") + + when (returnType) { + PluginMethod.Companion.RETURN_NONE -> lines.add( + "return w.Capacitor.nativeCallback('" + + plugin.id + + "', '" + + method.getName() + + "', " + + CATCHALL_OPTIONS_PARAM + + ")" + ) + + PluginMethod.Companion.RETURN_PROMISE -> lines.add( + "return w.Capacitor.nativePromise('" + plugin.id + "', '" + method.getName() + "', " + CATCHALL_OPTIONS_PARAM + ")" + ) + + PluginMethod.Companion.RETURN_CALLBACK -> lines.add( + "return w.Capacitor.nativeCallback('" + + plugin.id + + "', '" + + method.getName() + + "', " + + CATCHALL_OPTIONS_PARAM + + ", " + + CALLBACK_PARAM + + ")" + ) + + else -> {} + } + lines.add("}") + + return TextUtils.join("\n", lines) + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.java deleted file mode 100644 index 14b6043a..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.java +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.kt new file mode 100644 index 00000000..a1557b7d --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSExportException.kt @@ -0,0 +1,9 @@ +package com.getcapacitor + +class JSExportException : Exception { + constructor(s: String?) : super(s) + + constructor(t: Throwable?) : super(t) + + constructor(s: String?, t: Throwable?) : super(s, t) +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java deleted file mode 100644 index a3871f7b..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.java +++ /dev/null @@ -1,107 +0,0 @@ -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 = ""; - String html = this.readAssetStream(responseStream); - - // Insert the js string at the position after or before using StringBuilder - StringBuilder modifiedHtml = new StringBuilder(html); - if (html.contains("")) { - modifiedHtml.insert(html.indexOf("") + "".length(), "\n" + js + "\n"); - html = modifiedHtml.toString(); - } else if (html.contains("")) { - modifiedHtml.insert(html.indexOf(""), "\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 ""; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.kt new file mode 100644 index 00000000..0f516013 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSInjector.kt @@ -0,0 +1,100 @@ +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. + */ +internal class JSInjector( + private val globalJS: String, + private val bridgeJS: String, + private val pluginJS: String, + private val cordovaJS: String, + private val cordovaPluginsJS: String, + private val cordovaPluginsFileJS: String, + private val localUrlJS: String +) { + val scriptString: String + /** + * Generates injectable JS content. + * This may be used in other forms of injecting that aren't using an InputStream. + * @return + */ + get() = (""" + $globalJS + + $localUrlJS + + $bridgeJS + + $pluginJS + + $cordovaJS + + $cordovaPluginsFileJS + + $cordovaPluginsJS + """.trimIndent() + ) + + /** + * Given an InputStream from the web server, prepend it with + * our JS stream + * @param responseStream + * @return + */ + fun getInjectedStream(responseStream: InputStream?): InputStream { + val js = "" + var html = this.readAssetStream(responseStream) + + // Insert the js string at the position after or before using StringBuilder + val modifiedHtml = StringBuilder(html) + if (html.contains("")) { + modifiedHtml.insert( + html.indexOf("") + "".length, """ + + $js + + """.trimIndent() + ) + html = modifiedHtml.toString() + } else if (html.contains("")) { + modifiedHtml.insert( + html.indexOf(""), """ + + $js + + """.trimIndent() + ) + html = modifiedHtml.toString() + } else { + Logger.Companion.error("Unable to inject Capacitor, Plugins won't work") + } + return ByteArrayInputStream(html.toByteArray(StandardCharsets.UTF_8)) + } + + private fun readAssetStream(stream: InputStream?): String { + try { + val bufferSize = 1024 + val buffer = CharArray(bufferSize) + val out = StringBuilder() + val `in`: Reader = InputStreamReader(stream, StandardCharsets.UTF_8) + while (true) { + val rsz = `in`.read(buffer, 0, buffer.size) + if (rsz < 0) break + out.append(buffer, 0, rsz) + } + return out.toString() + } catch (e: Exception) { + Logger.Companion.error("Unable to process HTML asset file. This is a fatal error", e) + } + + return "" + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.java deleted file mode 100644 index 0e987076..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.java +++ /dev/null @@ -1,164 +0,0 @@ -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 keysIter = obj.keys(); - List 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 keysIter = ((JSONObject) obj).keys(); - List 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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.kt new file mode 100644 index 00000000..e04a2c33 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSObject.kt @@ -0,0 +1,155 @@ +package com.getcapacitor + +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 + */ +open class JSObject : JSONObject { + constructor() : super() + + constructor(json: String?) : super(json) + + constructor(obj: JSONObject?, names: Array?) : super(obj, names) + + override fun getString(key: String): String? { + return getString(key, null) + } + + fun getString(key: String?, defaultValue: String?): String? { + try { + val value = super.getString(key) + if (!super.isNull(key)) { + return value + } + } catch (ex: JSONException) { + } + return defaultValue + } + + fun getInteger(key: String?): Int? { + return getInteger(key, null) + } + + fun getInteger(key: String?, defaultValue: Int?): Int? { + try { + return super.getInt(key) + } catch (e: JSONException) { + } + return defaultValue + } + + fun getBoolean(key: String?, defaultValue: Boolean?): Boolean? { + try { + return super.getBoolean(key) + } catch (e: JSONException) { + } + return defaultValue + } + + /** + * Fetch boolean from jsonObject + */ + fun getBool(key: String?): Boolean? { + return getBoolean(key, null) + } + + fun getJSObject(name: String?): JSObject? { + try { + return getJSObject(name, null) + } catch (e: JSONException) { + } + return null + } + + @Throws(JSONException::class) + fun getJSObject(name: String?, defaultValue: JSObject?): JSObject? { + try { + val obj = get(name) + if (obj is JSONObject) { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + + return JSObject(obj, keys.toTypedArray()) + } + } catch (ex: JSONException) { + } + return defaultValue + } + + override fun put(key: String, value: Boolean): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + override fun put(key: String, value: Int): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + override fun put(key: String, value: Long): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + override fun put(key: String, value: Double): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + override fun put(key: String, value: Any): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + fun put(key: String?, value: String?): JSObject { + try { + super.put(key, value) + } catch (ex: JSONException) { + } + return this + } + + @Throws(JSONException::class) + fun putSafe(key: String?, value: Any?): JSObject { + return super.put(key, value) as JSObject + } + + companion object { + /** + * Convert a pathetic JSONObject into a JSObject + * @param obj + */ + @Throws(JSONException::class) + fun fromJSONObject(obj: JSONObject): JSObject { + val keysIter = obj.keys() + val keys: MutableList = ArrayList() + while (keysIter.hasNext()) { + keys.add(keysIter.next()) + } + + return JSObject(obj, keys.toTypedArray()) + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.java deleted file mode 100644 index d97ba91b..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.java +++ /dev/null @@ -1,65 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.kt new file mode 100644 index 00000000..e57395ec --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/JSValue.kt @@ -0,0 +1,62 @@ +package com.getcapacitor + +import org.json.JSONException + +/** + * Represents a single user-data value of any type on the capacitor PluginCall object. + */ +class JSValue(call: PluginCall, name: String) { + /** + * Returns the coerced but uncasted underlying value. + */ + @JvmField + val value: Any + + /** + * @param call The capacitor plugin call, used for accessing the value safely. + * @param name The name of the property to access. + */ + init { + this.value = this.toValue(call, name) + } + + override fun toString(): String { + return value.toString() + } + + /** + * Returns the underlying value as a JSObject, or throwing if it cannot. + * + * @throws JSONException If the underlying value is not a JSObject. + */ + @Throws(JSONException::class) + fun toJSObject(): JSObject { + if (value is JSObject) return value + throw 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. + */ + @Throws(JSONException::class) + fun toJSArray(): JSArray { + if (value is JSArray) return value + throw 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 fun toValue(call: PluginCall, name: String): Any { + var value: Any? = 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.data.opt(name) + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.java deleted file mode 100644 index 9d24fedd..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.java +++ /dev/null @@ -1,103 +0,0 @@ -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(); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.kt new file mode 100644 index 00000000..c80cbb2c --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Logger.kt @@ -0,0 +1,109 @@ +package com.getcapacitor + +import android.text.TextUtils +import android.util.Log + +class Logger { + private fun loadConfig(config: CapConfig) { + Companion.config = config + } + + companion object { + const val LOG_TAG_CORE: String = "Capacitor" + var config: CapConfig? = null + + private var instance: Logger? = null + get() { + if (field == null) { + field = Logger() + } + return field + } + + fun init(config: CapConfig) { + instance!!.loadConfig(config) + } + + @JvmStatic + fun tags(vararg subtags: String?): String { + if (subtags != null && subtags.size > 0) { + return LOG_TAG_CORE + "/" + TextUtils.join("/", subtags) + } + + return LOG_TAG_CORE + } + + fun verbose(message: String?) { + verbose(LOG_TAG_CORE, message) + } + + fun verbose(tag: String?, message: String?) { + if (!shouldLog()) { + return + } + + Log.v(tag, message!!) + } + + fun debug(message: String?) { + debug(LOG_TAG_CORE, message) + } + + @JvmStatic + fun debug(tag: String?, message: String?) { + if (!shouldLog()) { + return + } + + Log.d(tag, message!!) + } + + fun info(message: String?) { + info(LOG_TAG_CORE, message) + } + + @JvmStatic + fun info(tag: String?, message: String?) { + if (!shouldLog()) { + return + } + + Log.i(tag, message!!) + } + + fun warn(message: String?) { + warn(LOG_TAG_CORE, message) + } + + fun warn(tag: String?, message: String?) { + if (!shouldLog()) { + return + } + + Log.w(tag, message!!) + } + + @JvmStatic + fun error(message: String?) { + error(LOG_TAG_CORE, message, null) + } + + @JvmStatic + fun error(message: String?, e: Throwable?) { + error(LOG_TAG_CORE, message, e) + } + + @JvmStatic + fun error(tag: String?, message: String?, e: Throwable?) { + if (!shouldLog()) { + return + } + + Log.e(tag, message, e) + } + + fun shouldLog(): Boolean { + return config == null || config!!.isLoggingEnabled + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java deleted file mode 100644 index b71124e8..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.java +++ /dev/null @@ -1,159 +0,0 @@ -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); - } - ); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.kt new file mode 100644 index 00000000..3655b3ad --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/MessageHandler.kt @@ -0,0 +1,176 @@ +package com.getcapacitor + +import android.net.Uri +import android.webkit.JavascriptInterface +import android.webkit.WebView +import androidx.webkit.JavaScriptReplyProxy +import androidx.webkit.WebMessageCompat +import androidx.webkit.WebViewCompat +import androidx.webkit.WebViewCompat.WebMessageListener +import androidx.webkit.WebViewFeature +import org.apache.cordova.PluginManager + +/** + * MessageHandler handles messages from the WebView, dispatching them + * to plugins. + */ +class MessageHandler( + private val bridge: Bridge, + private val webView: WebView, + private val cordovaPluginManager: PluginManager +) { + private var javaScriptReplyProxy: JavaScriptReplyProxy? = null + + init { + if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && !bridge.config.isUsingLegacyBridge) { + val capListener = + WebMessageListener { view: WebView?, message: WebMessageCompat, sourceOrigin: Uri?, isMainFrame: Boolean, replyProxy: JavaScriptReplyProxy? -> + if (isMainFrame) { + postMessage(message.data) + javaScriptReplyProxy = replyProxy + } else { + Logger.Companion.warn("Plugin execution is allowed in Main Frame only") + } + } + try { + WebViewCompat.addWebMessageListener( + webView, + "androidBridge", + bridge.getAllowedOriginRules(), + capListener + ) + } catch (ex: Exception) { + 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 + @Suppress("unused") + fun postMessage(jsonStr: String?) { + try { + val postData = JSObject(jsonStr) + + val type = postData.getString("type") + + val typeIsNotNull = type != null + val isCordovaPlugin = typeIsNotNull && type == "cordova" + val isJavaScriptError = typeIsNotNull && type == "js.error" + + val callbackId = postData.getString("callbackId") + + if (isCordovaPlugin) { + val service = postData.getString("service") + val action = postData.getString("action") + val actionArgs = postData.getString("actionArgs") + + Logger.Companion.verbose( + Logger.Companion.tags("Plugin"), + "To native (Cordova plugin): callbackId: " + + callbackId + + ", service: " + + service + + ", action: " + + action + + ", actionArgs: " + + actionArgs + ) + + this.callCordovaPluginMethod(callbackId, service, action, actionArgs) + } else if (isJavaScriptError) { + Logger.Companion.error("JavaScript Error: $jsonStr") + } else { + val pluginId = postData.getString("pluginId") + val methodName = postData.getString("methodName") + val methodData = postData.getJSObject("options", JSObject()) + + Logger.Companion.verbose( + Logger.Companion.tags("Plugin"), + "To native (Capacitor plugin): callbackId: $callbackId, pluginId: $pluginId, methodName: $methodName" + ) + + this.callPluginMethod(callbackId, pluginId, methodName, methodData) + } + } catch (ex: Exception) { + Logger.Companion.error("Post message error:", ex) + } + } + + fun sendResponseMessage( + call: PluginCall, + successResult: PluginResult?, + errorResult: PluginResult? + ) { + try { + val data = PluginResult() + data.put("save", call.isKeptAlive) + data.put("callbackId", call.callbackId) + data.put("pluginId", call.pluginId) + data.put("methodName", call.methodName) + + val pluginResultInError = errorResult != null + if (pluginResultInError) { + data.put("success", false) + data.put("error", errorResult) + Logger.Companion.debug("Sending plugin error: $data") + } else { + data.put("success", true) + if (successResult != null) { + data.put("data", successResult) + } + } + + val isValidCallbackId = call.callbackId != PluginCall.Companion.CALLBACK_ID_DANGLING + if (isValidCallbackId) { + if (bridge.config.isUsingLegacyBridge) { + legacySendResponseMessage(data) + } else if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER) && javaScriptReplyProxy != null) { + javaScriptReplyProxy.postMessage(data.toString()) + } else { + legacySendResponseMessage(data) + } + } else { + bridge.app.fireRestoredResult(data) + } + } catch (ex: Exception) { + Logger.Companion.error("sendResponseMessage: error: $ex") + } + if (!call.isKeptAlive) { + call.release(bridge) + } + } + + private fun legacySendResponseMessage(data: PluginResult) { + val runScript = "window.Capacitor.fromNative($data)" + val webView = this.webView + webView.post { webView.evaluateJavascript(runScript, null) } + } + + private fun callPluginMethod( + callbackId: String?, + pluginId: String?, + methodName: String?, + methodData: JSObject? + ) { + val call = PluginCall(this, pluginId, callbackId, methodName, methodData) + bridge.callPluginMethod(pluginId!!, methodName!!, call) + } + + private fun callCordovaPluginMethod( + callbackId: String?, + service: String?, + action: String?, + actionArgs: String? + ) { + bridge.execute { + cordovaPluginManager.exec(service, action, callbackId, actionArgs) + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.kt similarity index 53% rename from @capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java rename to @capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.kt index c4307624..62c84056 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/NativePlugin.kt @@ -1,37 +1,28 @@ -package com.getcapacitor; - -import com.getcapacitor.annotation.CapacitorPlugin; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +package com.getcapacitor /** * Base annotation for all Plugins - * @deprecated - *

Use {@link CapacitorPlugin} instead */ -@Retention(RetentionPolicy.RUNTIME) -@Deprecated -public @interface NativePlugin { +@Retention(AnnotationRetention.RUNTIME) +@Deprecated("

Use {@link CapacitorPlugin} instead") +annotation class NativePlugin( /** * Request codes this plugin uses and responds to, in order to tie * Android events back the plugin to handle */ - int[] requestCodes() default {}; - + val requestCodes: IntArray = [], /** * Permissions this plugin needs, in order to make permission requests * easy if the plugin only needs basic permission prompting */ - String[] permissions() default {}; - + val permissions: Array = [], /** * The request code to use when automatically requesting permissions */ - int permissionRequestCode() default 9000; - + val permissionRequestCode: Int = 9000, /** * A custom name for the plugin, otherwise uses the * simple class name. */ - String name() default ""; -} + val name: String = "" +) diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.java deleted file mode 100644 index 382cff71..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.java +++ /dev/null @@ -1,31 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.kt new file mode 100644 index 00000000..5b2c7c58 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PermissionState.kt @@ -0,0 +1,25 @@ +package com.getcapacitor + +/** + * Represents the state of a permission + * + * @since 3.0.0 + */ +enum class PermissionState(private val state: String) { + GRANTED("granted"), + DENIED("denied"), + PROMPT("prompt"), + PROMPT_WITH_RATIONALE("prompt-with-rationale"); + + override fun toString(): String { + return state + } + + companion object { + fun byState(state: String): PermissionState { + var state = state + state = state.uppercase().replace('-', '_') + return valueOf(state) + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.java deleted file mode 100644 index d8a3e82a..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.java +++ /dev/null @@ -1,1046 +0,0 @@ -package com.getcapacitor; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.res.Configuration; -import android.net.Uri; -import android.os.Bundle; -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.app.ActivityCompat; -import com.getcapacitor.annotation.ActivityCallback; -import com.getcapacitor.annotation.CapacitorPlugin; -import com.getcapacitor.annotation.Permission; -import com.getcapacitor.annotation.PermissionCallback; -import com.getcapacitor.util.PermissionHelper; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CopyOnWriteArrayList; -import org.json.JSONException; - -/** - * Plugin is the base class for all plugins, containing a number of - * convenient features for interacting with the {@link Bridge}, managing - * plugin permissions, tracking lifecycle events, and more. - * - * You should inherit from this class when creating new plugins, along with - * adding the {@link CapacitorPlugin} annotation to add additional required - * metadata about the Plugin - */ -public class Plugin { - - // The key we will use inside of a persisted Bundle for the JSON blob - // for a plugin call options. - private static final String BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json"; - - // Reference to the Bridge - protected Bridge bridge; - - // Reference to the PluginHandle wrapper for this Plugin - protected PluginHandle handle; - - /** - * A way for plugins to quickly save a call that they will need to reference - * between activity/permissions starts/requests - * - * @deprecated store calls on the bridge using the methods - * {@link com.getcapacitor.Bridge#saveCall(PluginCall)}, - * {@link com.getcapacitor.Bridge#getSavedCall(String)} and - * {@link com.getcapacitor.Bridge#releaseCall(PluginCall)} - */ - @Deprecated - protected PluginCall savedLastCall; - - // Stored event listeners - private final Map> eventListeners; - - /** - * Launchers used by the plugin to handle activity results - */ - private final Map> activityLaunchers = new HashMap<>(); - - /** - * Launchers used by the plugin to handle permission results - */ - private final Map> permissionLaunchers = new HashMap<>(); - - private String lastPluginCallId; - - // Stored results of an event if an event was fired and - // no listeners were attached yet. Only stores the last value. - private final Map> retainedEventArguments; - - public Plugin() { - eventListeners = new HashMap<>(); - retainedEventArguments = new HashMap<>(); - } - - /** - * Called when the plugin has been connected to the bridge - * and is ready to start initializing. - */ - public void load() {} - - /** - * Registers activity result launchers defined on plugins, used for permission requests and - * activities started for result. - */ - void initializeActivityLaunchers() { - List pluginClassMethods = new ArrayList<>(); - for ( - Class pluginCursor = getClass(); - !pluginCursor.getName().equals(Object.class.getName()); - pluginCursor = pluginCursor.getSuperclass() - ) { - pluginClassMethods.addAll(Arrays.asList(pluginCursor.getDeclaredMethods())); - } - - for (final Method method : pluginClassMethods) { - if (method.isAnnotationPresent(ActivityCallback.class)) { - // register callbacks annotated with ActivityCallback for activity results - ActivityResultLauncher launcher = bridge.registerForActivityResult( - new ActivityResultContracts.StartActivityForResult(), - result -> triggerActivityCallback(method, result) - ); - - activityLaunchers.put(method.getName(), launcher); - } else if (method.isAnnotationPresent(PermissionCallback.class)) { - // register callbacks annotated with PermissionCallback for permission results - ActivityResultLauncher launcher = bridge.registerForActivityResult( - new ActivityResultContracts.RequestMultiplePermissions(), - permissions -> triggerPermissionCallback(method, permissions) - ); - - permissionLaunchers.put(method.getName(), launcher); - } - } - } - - private void triggerPermissionCallback(Method method, Map permissionResultMap) { - PluginCall savedCall = bridge.getPermissionCall(handle.getId()); - - // validate permissions and invoke the permission result callback - if (bridge.validatePermissions(this, savedCall, permissionResultMap)) { - try { - method.setAccessible(true); - method.invoke(this, savedCall); - } catch (IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - } - } - } - - private void triggerActivityCallback(Method method, ActivityResult result) { - PluginCall savedCall = bridge.getSavedCall(lastPluginCallId); - if (savedCall == null) { - savedCall = bridge.getPluginCallForLastActivity(); - } - // invoke the activity result callback - try { - method.setAccessible(true); - method.invoke(this, savedCall, result); - } catch (IllegalAccessException | InvocationTargetException e) { - e.printStackTrace(); - } - } - - /** - * Start activity for result with the provided Intent and resolve with the provided callback method name. - *

- * If there is no registered activity callback for the method name passed in, the call will - * be rejected. Make sure a valid activity result callback method is registered using the - * {@link ActivityCallback} annotation. - * - * @param call the plugin call - * @param intent the intent used to start an activity - * @param callbackName the name of the callback to run when the launched activity is finished - * @since 3.0.0 - */ - public void startActivityForResult(PluginCall call, Intent intent, String callbackName) { - ActivityResultLauncher activityResultLauncher = getActivityLauncherOrReject(call, callbackName); - if (activityResultLauncher == null) { - // return when null since call was rejected in getLauncherOrReject - return; - } - bridge.setPluginCallForLastActivity(call); - lastPluginCallId = call.getCallbackId(); - bridge.saveCall(call); - activityResultLauncher.launch(intent); - } - - private void permissionActivityResult(PluginCall call, String[] permissionStrings, String callbackName) { - ActivityResultLauncher permissionResultLauncher = getPermissionLauncherOrReject(call, callbackName); - if (permissionResultLauncher == null) { - // return when null since call was rejected in getLauncherOrReject - return; - } - - bridge.savePermissionCall(call); - permissionResultLauncher.launch(permissionStrings); - } - - /** - * Get the main {@link Context} for the current Activity (your app) - * @return the Context for the current activity - */ - public Context getContext() { - return this.bridge.getContext(); - } - - /** - * Get the main {@link Activity} for the app - * @return the Activity for the current app - */ - public AppCompatActivity getActivity() { - return this.bridge.getActivity(); - } - - /** - * Set the Bridge instance for this plugin - * @param bridge - */ - public void setBridge(Bridge bridge) { - this.bridge = bridge; - } - - /** - * Get the Bridge instance for this plugin - */ - public Bridge getBridge() { - return this.bridge; - } - - /** - * Set the wrapper {@link PluginHandle} instance for this plugin that - * contains additional metadata about the Plugin instance (such - * as indexed methods for reflection, and {@link CapacitorPlugin} annotation data). - * @param pluginHandle - */ - public void setPluginHandle(PluginHandle pluginHandle) { - this.handle = pluginHandle; - } - - /** - * Return the wrapper {@link PluginHandle} for this plugin. - * - * This wrapper contains additional metadata about the plugin instance, - * such as indexed methods for reflection, and {@link CapacitorPlugin} annotation data). - * @return - */ - public PluginHandle getPluginHandle() { - return this.handle; - } - - /** - * Get the root App ID - * @return - */ - public String getAppId() { - return getContext().getPackageName(); - } - - /** - * Called to save a {@link PluginCall} in order to reference it - * later, such as in an activity or permissions result handler - * @deprecated use {@link Bridge#saveCall(PluginCall)} - * - * @param lastCall - */ - @Deprecated - public void saveCall(PluginCall lastCall) { - this.savedLastCall = lastCall; - } - - /** - * Set the last saved call to null to free memory - * @deprecated use {@link PluginCall#release(Bridge)} - */ - @Deprecated - public void freeSavedCall() { - this.savedLastCall.release(bridge); - this.savedLastCall = null; - } - - /** - * Get the last saved call, if any - * @deprecated use {@link Bridge#getSavedCall(String)} - * - * @return - */ - @Deprecated - public PluginCall getSavedCall() { - return this.savedLastCall; - } - - /** - * Get the config options for this plugin. - * - * @return a config object representing the plugin config options, or an empty config - * if none exists - */ - public PluginConfig getConfig() { - return bridge.getConfig().getPluginConfiguration(handle.getId()); - } - - /** - * Get the value for a key on the config for this plugin. - * @deprecated use {@link #getConfig()} and access config values using the methods available - * depending on the type. - * - * @param key the key for the config value - * @return some object containing the value from the config - */ - @Deprecated - public Object getConfigValue(String key) { - try { - PluginConfig pluginConfig = getConfig(); - return pluginConfig.getConfigJSON().get(key); - } catch (JSONException ex) { - return null; - } - } - - /** - * Check whether any of the given permissions has been defined in the AndroidManifest.xml - * @deprecated use {@link #isPermissionDeclared(String)} - * - * @param permissions - * @return - */ - @Deprecated - public boolean hasDefinedPermissions(String[] permissions) { - for (String permission : permissions) { - if (!PermissionHelper.hasDefinedPermission(getContext(), permission)) { - return false; - } - } - return true; - } - - /** - * Check if all annotated permissions have been defined in the AndroidManifest.xml - * @deprecated use {@link #isPermissionDeclared(String)} - * - * @return true if permissions are all defined in the Manifest - */ - @Deprecated - public boolean hasDefinedRequiredPermissions() { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - if (annotation == null) { - // Check for legacy plugin annotation, @NativePlugin - NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); - return hasDefinedPermissions(legacyAnnotation.permissions()); - } else { - for (Permission perm : annotation.permissions()) { - for (String permString : perm.strings()) { - if (!PermissionHelper.hasDefinedPermission(getContext(), permString)) { - return false; - } - } - } - } - - return true; - } - - /** - * Checks if the given permission alias is correctly declared in AndroidManifest.xml - * @param alias a permission alias defined on the plugin - * @return true only if all permissions associated with the given alias are declared in the manifest - */ - public boolean isPermissionDeclared(String alias) { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - if (annotation != null) { - for (Permission perm : annotation.permissions()) { - if (alias.equalsIgnoreCase(perm.alias())) { - boolean result = true; - for (String permString : perm.strings()) { - result = result && PermissionHelper.hasDefinedPermission(getContext(), permString); - } - - return result; - } - } - } - - Logger.error(String.format("isPermissionDeclared: No alias defined for %s " + "or missing @CapacitorPlugin annotation.", alias)); - return false; - } - - /** - * Check whether the given permission has been granted by the user - * @deprecated use {@link #getPermissionState(String)} and {@link #getPermissionStates()} to get - * the states of permissions defined on the Plugin in conjunction with the @CapacitorPlugin - * annotation. Use the Android API {@link ActivityCompat#checkSelfPermission(Context, String)} - * methods to check permissions with Android permission strings - * - * @param permission - * @return - */ - @Deprecated - public boolean hasPermission(String permission) { - return ActivityCompat.checkSelfPermission(this.getContext(), permission) == PackageManager.PERMISSION_GRANTED; - } - - /** - * If the plugin annotation specified a set of permissions, this method checks if each is - * granted - * @deprecated use {@link #getPermissionState(String)} or {@link #getPermissionStates()} to - * check whether permissions are granted or not - * - * @return - */ - @Deprecated - public boolean hasRequiredPermissions() { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - if (annotation == null) { - // Check for legacy plugin annotation, @NativePlugin - NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); - for (String perm : legacyAnnotation.permissions()) { - if (ActivityCompat.checkSelfPermission(this.getContext(), perm) != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - - return true; - } - - for (Permission perm : annotation.permissions()) { - for (String permString : perm.strings()) { - if (ActivityCompat.checkSelfPermission(this.getContext(), permString) != PackageManager.PERMISSION_GRANTED) { - return false; - } - } - } - - return true; - } - - /** - * Request all of the specified permissions in the CapacitorPlugin annotation (if any) - * - * If there is no registered permission callback for the PluginCall passed in, the call will - * be rejected. Make sure a valid permission callback method is registered using the - * {@link PermissionCallback} annotation. - * - * @since 3.0.0 - * @param call the plugin call - * @param callbackName the name of the callback to run when the permission request is complete - */ - protected void requestAllPermissions(@NonNull PluginCall call, @NonNull String callbackName) { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - if (annotation != null) { - HashSet perms = new HashSet<>(); - for (Permission perm : annotation.permissions()) { - perms.addAll(Arrays.asList(perm.strings())); - } - - permissionActivityResult(call, perms.toArray(new String[0]), callbackName); - } - } - - /** - * Request permissions using an alias defined on the plugin. - * - * If there is no registered permission callback for the PluginCall passed in, the call will - * be rejected. Make sure a valid permission callback method is registered using the - * {@link PermissionCallback} annotation. - * - * @param alias an alias defined on the plugin - * @param call the plugin call involved in originating the request - * @param callbackName the name of the callback to run when the permission request is complete - */ - protected void requestPermissionForAlias(@NonNull String alias, @NonNull PluginCall call, @NonNull String callbackName) { - requestPermissionForAliases(new String[] { alias }, call, callbackName); - } - - /** - * Request permissions using aliases defined on the plugin. - * - * If there is no registered permission callback for the PluginCall passed in, the call will - * be rejected. Make sure a valid permission callback method is registered using the - * {@link PermissionCallback} annotation. - * - * @param aliases a set of aliases defined on the plugin - * @param call the plugin call involved in originating the request - * @param callbackName the name of the callback to run when the permission request is complete - */ - protected void requestPermissionForAliases(@NonNull String[] aliases, @NonNull PluginCall call, @NonNull String callbackName) { - if (aliases.length == 0) { - Logger.error("No permission alias was provided"); - return; - } - - String[] permissions = getPermissionStringsForAliases(aliases); - - if (permissions.length > 0) { - permissionActivityResult(call, permissions, callbackName); - } - } - - /** - * Gets the Android permission strings defined on the {@link CapacitorPlugin} annotation with - * the provided aliases. - * - * @param aliases aliases for permissions defined on the plugin - * @return Android permission strings associated with the provided aliases, if exists - */ - private String[] getPermissionStringsForAliases(@NonNull String[] aliases) { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - HashSet perms = new HashSet<>(); - for (Permission perm : annotation.permissions()) { - if (Arrays.asList(aliases).contains(perm.alias())) { - perms.addAll(Arrays.asList(perm.strings())); - } - } - - return perms.toArray(new String[0]); - } - - /** - * Gets the activity launcher associated with the calling methodName, or rejects the call if - * no registered launcher exists - * - * @param call the plugin call - * @param methodName the name of the activity callback method - * @return a launcher, or null if none found - */ - private @Nullable ActivityResultLauncher getActivityLauncherOrReject(PluginCall call, String methodName) { - ActivityResultLauncher activityLauncher = activityLaunchers.get(methodName); - - // if there is no registered launcher, reject the call with an error and return null - if (activityLauncher == null) { - String registerError = - "There is no ActivityCallback method registered for the name: %s. " + - "Please define a callback method annotated with @ActivityCallback " + - "that receives arguments: (PluginCall, ActivityResult)"; - registerError = String.format(Locale.US, registerError, methodName); - Logger.error(registerError); - call.reject(registerError); - return null; - } - - return activityLauncher; - } - - /** - * Gets the permission launcher associated with the calling methodName, or rejects the call if - * no registered launcher exists - * - * @param call the plugin call - * @param methodName the name of the permission callback method - * @return a launcher, or null if none found - */ - private @Nullable ActivityResultLauncher getPermissionLauncherOrReject(PluginCall call, String methodName) { - ActivityResultLauncher permissionLauncher = permissionLaunchers.get(methodName); - - // if there is no registered launcher, reject the call with an error and return null - if (permissionLauncher == null) { - String registerError = - "There is no PermissionCallback method registered for the name: %s. " + - "Please define a callback method annotated with @PermissionCallback " + - "that receives arguments: (PluginCall)"; - registerError = String.format(Locale.US, registerError, methodName); - Logger.error(registerError); - call.reject(registerError); - return null; - } - - return permissionLauncher; - } - - /** - * Request all of the specified permissions in the CapacitorPlugin annotation (if any) - * - * @deprecated use {@link #requestAllPermissions(PluginCall, String)} in conjunction with @CapacitorPlugin - */ - @Deprecated - public void pluginRequestAllPermissions() { - NativePlugin legacyAnnotation = handle.getLegacyPluginAnnotation(); - ActivityCompat.requestPermissions(getActivity(), legacyAnnotation.permissions(), legacyAnnotation.permissionRequestCode()); - } - - /** - * Helper for requesting a specific permission - * - * @param permission the permission to request - * @param requestCode the requestCode to use to associate the result with the plugin - * @deprecated use {@link #requestPermissionForAlias(String, PluginCall, String)} in conjunction with @CapacitorPlugin - */ - @Deprecated - public void pluginRequestPermission(String permission, int requestCode) { - ActivityCompat.requestPermissions(getActivity(), new String[] { permission }, requestCode); - } - - /** - * Helper for requesting specific permissions - * @deprecated use {@link #requestPermissionForAliases(String[], PluginCall, String)} in conjunction - * with @CapacitorPlugin - * - * @param permissions the set of permissions to request - * @param requestCode the requestCode to use to associate the result with the plugin - */ - @Deprecated - public void pluginRequestPermissions(String[] permissions, int requestCode) { - ActivityCompat.requestPermissions(getActivity(), permissions, requestCode); - } - - /** - * Get the permission state for the provided permission alias. - * - * @param alias the permission alias to get - * @return the state of the provided permission alias or null - */ - public PermissionState getPermissionState(String alias) { - return getPermissionStates().get(alias); - } - - /** - * Helper to check all permissions defined on a plugin and see the state of each. - * - * @since 3.0.0 - * @return A mapping of permission aliases to the associated granted status. - */ - public Map getPermissionStates() { - return bridge.getPermissionStates(this); - } - - /** - * Add a listener for the given event - * @param eventName - * @param call - */ - private void addEventListener(String eventName, PluginCall call) { - List listeners = eventListeners.get(eventName); - if (listeners == null || listeners.isEmpty()) { - listeners = new ArrayList<>(); - eventListeners.put(eventName, listeners); - - // Must add the call before sending retained arguments - listeners.add(call); - - sendRetainedArgumentsForEvent(eventName); - } else { - listeners.add(call); - } - } - - /** - * Remove a listener from the given event - * @param eventName - * @param call - */ - private void removeEventListener(String eventName, PluginCall call) { - List listeners = eventListeners.get(eventName); - if (listeners == null) { - return; - } - - listeners.remove(call); - } - - /** - * Notify all listeners that an event occurred - * @param eventName - * @param data - */ - protected void notifyListeners(String eventName, JSObject data, boolean retainUntilConsumed) { - Logger.verbose(getLogTag(), "Notifying listeners for event " + eventName); - List listeners = eventListeners.get(eventName); - if (listeners == null || listeners.isEmpty()) { - Logger.debug(getLogTag(), "No listeners found for event " + eventName); - if (retainUntilConsumed) { - List argList = retainedEventArguments.get(eventName); - - if (argList == null) { - argList = new ArrayList(); - } - - argList.add(data); - retainedEventArguments.put(eventName, argList); - } - return; - } - - CopyOnWriteArrayList listenersCopy = new CopyOnWriteArrayList(listeners); - for (PluginCall call : listenersCopy) { - call.resolve(data); - } - } - - /** - * Notify all listeners that an event occurred - * This calls {@link Plugin#notifyListeners(String, JSObject, boolean)} - * with retainUntilConsumed set to false - * @param eventName - * @param data - */ - protected void notifyListeners(String eventName, JSObject data) { - notifyListeners(eventName, data, false); - } - - /** - * Check if there are any listeners for the given event - */ - protected boolean hasListeners(String eventName) { - List listeners = eventListeners.get(eventName); - if (listeners == null) { - return false; - } - return !listeners.isEmpty(); - } - - /** - * Send retained arguments (if any) for this event. This - * is called only when the first listener for an event is added - * @param eventName - */ - private void sendRetainedArgumentsForEvent(String eventName) { - // copy retained args and null source to prevent potential race conditions - List retainedArgs = retainedEventArguments.get(eventName); - if (retainedArgs == null) { - return; - } - - retainedEventArguments.remove(eventName); - - for (JSObject retained : retainedArgs) { - notifyListeners(eventName, retained); - } - } - - /** - * Exported plugin call for adding a listener to this plugin - * @param call - */ - @SuppressWarnings("unused") - @PluginMethod(returnType = PluginMethod.RETURN_NONE) - public void addListener(PluginCall call) { - String eventName = call.getString("eventName"); - call.setKeepAlive(true); - addEventListener(eventName, call); - } - - /** - * Exported plugin call to remove a listener from this plugin - * @param call - */ - @SuppressWarnings("unused") - @PluginMethod(returnType = PluginMethod.RETURN_NONE) - public void removeListener(PluginCall call) { - String eventName = call.getString("eventName"); - String callbackId = call.getString("callbackId"); - PluginCall savedCall = bridge.getSavedCall(callbackId); - if (savedCall != null) { - removeEventListener(eventName, savedCall); - bridge.releaseCall(savedCall); - } - } - - /** - * Exported plugin call to remove all listeners from this plugin - * @param call - */ - @SuppressWarnings("unused") - @PluginMethod(returnType = PluginMethod.RETURN_PROMISE) - public void removeAllListeners(PluginCall call) { - eventListeners.clear(); - call.resolve(); - } - - /** - * Exported plugin call for checking the granted status for each permission - * declared on the plugin. This plugin call responds with a mapping of permissions to - * the associated granted status. - * - * @since 3.0.0 - */ - @PluginMethod - @PermissionCallback - public void checkPermissions(PluginCall pluginCall) { - Map permissionsResult = getPermissionStates(); - - if (permissionsResult.size() == 0) { - // if no permissions are defined on the plugin, resolve undefined - pluginCall.resolve(); - } else { - JSObject permissionsResultJSON = new JSObject(); - for (Map.Entry entry : permissionsResult.entrySet()) { - permissionsResultJSON.put(entry.getKey(), entry.getValue()); - } - - pluginCall.resolve(permissionsResultJSON); - } - } - - /** - * Exported plugin call to request all permissions for this plugin. - * To manually request permissions within a plugin use: - * {@link #requestAllPermissions(PluginCall, String)}, or - * {@link #requestPermissionForAlias(String, PluginCall, String)}, or - * {@link #requestPermissionForAliases(String[], PluginCall, String)} - * - * @param call the plugin call - */ - @PluginMethod - public void requestPermissions(PluginCall call) { - CapacitorPlugin annotation = handle.getPluginAnnotation(); - if (annotation == null) { - handleLegacyPermission(call); - } else { - // handle permission requests for plugins defined with @CapacitorPlugin (since 3.0.0) - String[] permAliases = null; - Set autoGrantPerms = new HashSet<>(); - - // If call was made with a list of specific permission aliases to request, save them - // to be requested - JSArray providedPerms = call.getArray("permissions"); - List providedPermsList = null; - - if (providedPerms != null) { - try { - providedPermsList = providedPerms.toList(); - } catch (JSONException ignore) { - // do nothing - } - } - - // If call was made without any custom permissions, request all from plugin annotation - Set aliasSet = new HashSet<>(); - if (providedPermsList == null || providedPermsList.isEmpty()) { - for (Permission perm : annotation.permissions()) { - // If a permission is defined with no permission strings, separate it for auto-granting. - // Otherwise, the alias is added to the list to be requested. - if (perm.strings().length == 0 || (perm.strings().length == 1 && perm.strings()[0].isEmpty())) { - if (!perm.alias().isEmpty()) { - autoGrantPerms.add(perm.alias()); - } - } else { - aliasSet.add(perm.alias()); - } - } - - permAliases = aliasSet.toArray(new String[0]); - } else { - for (Permission perm : annotation.permissions()) { - if (providedPermsList.contains(perm.alias())) { - aliasSet.add(perm.alias()); - } - } - - if (aliasSet.isEmpty()) { - call.reject("No valid permission alias was requested of this plugin."); - } else { - permAliases = aliasSet.toArray(new String[0]); - } - } - - if (permAliases != null && permAliases.length > 0) { - // request permissions using provided aliases or all defined on the plugin - requestPermissionForAliases(permAliases, call, "checkPermissions"); - } else if (!autoGrantPerms.isEmpty()) { - // if the plugin only has auto-grant permissions, return all as GRANTED - JSObject permissionsResults = new JSObject(); - - for (String perm : autoGrantPerms) { - permissionsResults.put(perm, PermissionState.GRANTED.toString()); - } - - call.resolve(permissionsResults); - } else { - // no permissions are defined on the plugin, resolve undefined - call.resolve(); - } - } - } - - @SuppressWarnings("deprecation") - private void handleLegacyPermission(PluginCall call) { - // handle permission requests for plugins defined with @NativePlugin (prior to 3.0.0) - NativePlugin legacyAnnotation = this.handle.getLegacyPluginAnnotation(); - String[] perms = legacyAnnotation.permissions(); - if (perms.length > 0) { - saveCall(call); - pluginRequestPermissions(perms, legacyAnnotation.permissionRequestCode()); - } else { - call.resolve(); - } - } - - /** - * Handle request permissions result. A plugin using the deprecated {@link NativePlugin} - * should override this to handle the result, or this method will handle the result - * for our convenient requestPermissions call. - * @deprecated in favor of using callbacks in conjunction with {@link CapacitorPlugin} - * - * @param requestCode - * @param permissions - * @param grantResults - */ - @Deprecated - protected void handleRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { - if (!hasDefinedPermissions(permissions)) { - StringBuilder builder = new StringBuilder(); - builder.append("Missing the following permissions in AndroidManifest.xml:\n"); - String[] missing = PermissionHelper.getUndefinedPermissions(getContext(), permissions); - for (String perm : missing) { - builder.append(perm + "\n"); - } - savedLastCall.reject(builder.toString()); - savedLastCall = null; - } - } - - /** - * Called before the app is destroyed to give a plugin the chance to - * save the last call options for a saved plugin. By default, this - * method saves the full JSON blob of the options call. Since Bundle sizes - * may be limited, plugins that expect to be called with large data - * objects (such as a file), should override this method and selectively - * store option values in a {@link Bundle} to avoid exceeding limits. - * @return a new {@link Bundle} with fields set from the options of the last saved {@link PluginCall} - */ - protected Bundle saveInstanceState() { - PluginCall savedCall = bridge.getSavedCall(lastPluginCallId); - - if (savedCall == null) { - return null; - } - - Bundle ret = new Bundle(); - JSObject callData = savedCall.getData(); - - if (callData != null) { - ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString()); - } - - return ret; - } - - /** - * Called when the app is opened with a previously un-handled - * activity response. If the plugin that started the activity - * stored data in {@link Plugin#saveInstanceState()} then this - * method will be called to allow the plugin to restore from that. - * @param state - */ - protected void restoreState(Bundle state) {} - - /** - * Handle activity result, should be overridden by each plugin - * - * @deprecated provide a callback method using the {@link ActivityCallback} annotation and use - * the {@link #startActivityForResult(PluginCall, Intent, String)} method - * - * @param requestCode - * @param resultCode - * @param data - */ - @Deprecated - protected void handleOnActivityResult(int requestCode, int resultCode, Intent data) {} - - /** - * Handle onNewIntent - * @param intent - */ - protected void handleOnNewIntent(Intent intent) {} - - /** - * Handle onConfigurationChanged - * @param newConfig - */ - protected void handleOnConfigurationChanged(Configuration newConfig) {} - - /** - * Handle onStart - */ - protected void handleOnStart() {} - - /** - * Handle onRestart - */ - protected void handleOnRestart() {} - - /** - * Handle onResume - */ - protected void handleOnResume() {} - - /** - * Handle onPause - */ - protected void handleOnPause() {} - - /** - * Handle onStop - */ - protected void handleOnStop() {} - - /** - * Handle onDestroy - */ - protected void handleOnDestroy() {} - - /** - * Give the plugins a chance to take control when a URL is about to be loaded in the WebView. - * Returning true causes the WebView to abort loading the URL. - * Returning false causes the WebView to continue loading the URL. - * Returning null will defer to the default Capacitor policy - */ - @SuppressWarnings("unused") - public Boolean shouldOverrideLoad(Uri url) { - return null; - } - - /** - * Start a new Activity. - * - * Note: This method must be used by all plugins instead of calling - * {@link Activity#startActivityForResult} as it associates the plugin with - * any resulting data from the new Activity even if this app - * is destroyed by the OS (to free up memory, for example). - * @param intent - * @param resultCode - */ - @Deprecated - protected void startActivityForResult(PluginCall call, Intent intent, int resultCode) { - bridge.startActivityForPluginWithResult(call, intent, resultCode); - } - - /** - * Execute the given runnable on the Bridge's task handler - * @param runnable - */ - public void execute(Runnable runnable) { - bridge.execute(runnable); - } - - /** - * Shortcut for getting the plugin log tag - * @param subTags - */ - protected String getLogTag(String... subTags) { - return Logger.tags(subTags); - } - - /** - * Gets a plugin log tag with the child's class name as subTag. - */ - protected String getLogTag() { - return Logger.tags(this.getClass().getSimpleName()); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.kt new file mode 100644 index 00000000..6addf332 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/Plugin.kt @@ -0,0 +1,1077 @@ +package com.getcapacitor + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.content.res.Configuration +import android.net.Uri +import android.os.Bundle +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import com.getcapacitor.annotation.ActivityCallback +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.PermissionCallback +import com.getcapacitor.util.PermissionHelper +import org.json.JSONException +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import java.util.Arrays +import java.util.Locale +import java.util.concurrent.CopyOnWriteArrayList + +/** + * Plugin is the base class for all plugins, containing a number of + * convenient features for interacting with the [Bridge], managing + * plugin permissions, tracking lifecycle events, and more. + * + * You should inherit from this class when creating new plugins, along with + * adding the [CapacitorPlugin] annotation to add additional required + * metadata about the Plugin + */ +open class Plugin { + /** + * Get the Bridge instance for this plugin + */ + /** + * Set the Bridge instance for this plugin + * @param bridge + */ + // Reference to the Bridge + @JvmField + var bridge: Bridge? = null + + /** + * Return the wrapper [PluginHandle] for this plugin. + * + * This wrapper contains additional metadata about the plugin instance, + * such as indexed methods for reflection, and [CapacitorPlugin] annotation data). + * @return + */ + /** + * Set the wrapper [PluginHandle] instance for this plugin that + * contains additional metadata about the Plugin instance (such + * as indexed methods for reflection, and [CapacitorPlugin] annotation data). + * @param pluginHandle + */ + // Reference to the PluginHandle wrapper for this Plugin + var pluginHandle: PluginHandle? = null + + /** + * Get the last saved call, if any + * @return + */ + /** + * A way for plugins to quickly save a call that they will need to reference + * between activity/permissions starts/requests + * + */ + @get:Deprecated( + """use {@link Bridge#getSavedCall(String)} + + """ + ) + @Deprecated( + """store calls on the bridge using the methods + {@link com.getcapacitor.Bridge#saveCall(PluginCall)}, + {@link com.getcapacitor.Bridge#getSavedCall(String)} and + {@link com.getcapacitor.Bridge#releaseCall(PluginCall)}""" + ) + var savedCall: PluginCall? = null + /** + * Get the last saved call, if any + * @return + */ + @Deprecated( + """use {@link Bridge#getSavedCall(String)} + + """ + ) get + protected set + + // Stored event listeners + private val eventListeners: MutableMap> = + HashMap() + + /** + * Launchers used by the plugin to handle activity results + */ + private val activityLaunchers: MutableMap> = HashMap() + + /** + * Launchers used by the plugin to handle permission results + */ + private val permissionLaunchers: MutableMap>> = + HashMap() + + private var lastPluginCallId: String? = null + + // Stored results of an event if an event was fired and + // no listeners were attached yet. Only stores the last value. + private val retainedEventArguments: MutableMap> = + HashMap() + + /** + * Called when the plugin has been connected to the bridge + * and is ready to start initializing. + */ + open fun load() {} + + /** + * Registers activity result launchers defined on plugins, used for permission requests and + * activities started for result. + */ + fun initializeActivityLaunchers() { + val pluginClassMethods: MutableList = ArrayList() + var pluginCursor: Class<*> = javaClass + while (pluginCursor.name != Any::class.java.name + ) { + pluginClassMethods.addAll(Arrays.asList(*pluginCursor.declaredMethods)) + pluginCursor = pluginCursor.superclass + } + + for (method in pluginClassMethods) { + if (method.isAnnotationPresent(ActivityCallback::class.java)) { + // register callbacks annotated with ActivityCallback for activity results + val launcher = bridge!!.registerForActivityResult( + StartActivityForResult() + ) { result: ActivityResult -> triggerActivityCallback(method, result) } + + activityLaunchers[method.name] = launcher + } else if (method.isAnnotationPresent(PermissionCallback::class.java)) { + // register callbacks annotated with PermissionCallback for permission results + val launcher = + bridge!!.registerForActivityResult, Map>( + RequestMultiplePermissions() + ) { permissions: Map -> + triggerPermissionCallback( + method, + permissions + ) + } + + permissionLaunchers[method.name] = launcher + } + } + } + + private fun triggerPermissionCallback( + method: Method, + permissionResultMap: Map + ) { + val savedCall = bridge!!.getPermissionCall(pluginHandle.getId()) + + // validate permissions and invoke the permission result callback + if (bridge!!.validatePermissions(this, savedCall!!, permissionResultMap)) { + try { + method.isAccessible = true + method.invoke(this, savedCall) + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: InvocationTargetException) { + e.printStackTrace() + } + } + } + + private fun triggerActivityCallback(method: Method, result: ActivityResult) { + var savedCall = bridge!!.getSavedCall(lastPluginCallId) + if (savedCall == null) { + savedCall = bridge!!.getPluginCallForLastActivity() + } + // invoke the activity result callback + try { + method.isAccessible = true + method.invoke(this, savedCall, result) + } catch (e: IllegalAccessException) { + e.printStackTrace() + } catch (e: InvocationTargetException) { + e.printStackTrace() + } + } + + /** + * Start activity for result with the provided Intent and resolve with the provided callback method name. + * + * + * If there is no registered activity callback for the method name passed in, the call will + * be rejected. Make sure a valid activity result callback method is registered using the + * [ActivityCallback] annotation. + * + * @param call the plugin call + * @param intent the intent used to start an activity + * @param callbackName the name of the callback to run when the launched activity is finished + * @since 3.0.0 + */ + fun startActivityForResult(call: PluginCall, intent: Intent, callbackName: String) { + val activityResultLauncher = getActivityLauncherOrReject(call, callbackName) + ?: // return when null since call was rejected in getLauncherOrReject + return + bridge!!.setPluginCallForLastActivity(call) + lastPluginCallId = call.callbackId + bridge!!.saveCall(call) + activityResultLauncher.launch(intent) + } + + private fun permissionActivityResult( + call: PluginCall, + permissionStrings: Array, + callbackName: String + ) { + val permissionResultLauncher = getPermissionLauncherOrReject(call, callbackName) + ?: // return when null since call was rejected in getLauncherOrReject + return + + bridge!!.savePermissionCall(call) + permissionResultLauncher.launch(permissionStrings) + } + + val context: Context? + /** + * Get the main [Context] for the current Activity (your app) + * @return the Context for the current activity + */ + get() = bridge!!.getContext() + + val activity: AppCompatActivity? + /** + * Get the main [Activity] for the app + * @return the Activity for the current app + */ + get() = bridge!!.activity + + val appId: String + /** + * Get the root App ID + * @return + */ + get() = context!!.packageName + + /** + * Called to save a [PluginCall] in order to reference it + * later, such as in an activity or permissions result handler + * @param lastCall + */ + @Deprecated( + """use {@link Bridge#saveCall(PluginCall)} + + """ + ) + fun saveCall(lastCall: PluginCall?) { + this.savedCall = lastCall + } + + /** + * Set the last saved call to null to free memory + */ + @Deprecated("use {@link PluginCall#release(Bridge)}") + fun freeSavedCall() { + savedCall!!.release(bridge) + this.savedCall = null + } + + val config: PluginConfig? + /** + * Get the config options for this plugin. + * + * @return a config object representing the plugin config options, or an empty config + * if none exists + */ + get() = bridge!!.config.getPluginConfiguration(pluginHandle.getId()) + + /** + * Get the value for a key on the config for this plugin. + * @param key the key for the config value + * @return some object containing the value from the config + */ + @Deprecated( + """use {@link #getConfig()} and access config values using the methods available + depending on the type. + + """ + ) + fun getConfigValue(key: String?): Any? { + try { + val pluginConfig = config + return pluginConfig.getConfigJSON()[key] + } catch (ex: JSONException) { + return null + } + } + + /** + * Check whether any of the given permissions has been defined in the AndroidManifest.xml + * @param permissions + * @return + */ + @Deprecated( + """use {@link #isPermissionDeclared(String)} + + """ + ) + fun hasDefinedPermissions(permissions: Array): Boolean { + for (permission in permissions) { + if (!PermissionHelper.hasDefinedPermission(context, permission)) { + return false + } + } + return true + } + + /** + * Check if all annotated permissions have been defined in the AndroidManifest.xml + * @return true if permissions are all defined in the Manifest + */ + @Deprecated( + """use {@link #isPermissionDeclared(String)} + + """ + ) + fun hasDefinedRequiredPermissions(): Boolean { + val annotation = pluginHandle.getPluginAnnotation() + if (annotation == null) { + // Check for legacy plugin annotation, @NativePlugin + val legacyAnnotation = pluginHandle.getLegacyPluginAnnotation() + return hasDefinedPermissions(legacyAnnotation!!.permissions) + } else { + for (perm in annotation.permissions) { + for (permString in perm.strings) { + if (!PermissionHelper.hasDefinedPermission(context, permString)) { + return false + } + } + } + } + + return true + } + + /** + * Checks if the given permission alias is correctly declared in AndroidManifest.xml + * @param alias a permission alias defined on the plugin + * @return true only if all permissions associated with the given alias are declared in the manifest + */ + fun isPermissionDeclared(alias: String): Boolean { + val annotation = pluginHandle.getPluginAnnotation() + if (annotation != null) { + for (perm in annotation.permissions) { + if (alias.equals(perm.alias, ignoreCase = true)) { + var result = true + for (permString in perm.strings) { + result = result && PermissionHelper.hasDefinedPermission( + context, permString + ) + } + + return result + } + } + } + + Logger.Companion.error( + String.format( + "isPermissionDeclared: No alias defined for %s " + "or missing @CapacitorPlugin annotation.", + alias + ) + ) + return false + } + + /** + * Check whether the given permission has been granted by the user + * @param permission + * @return + */ + @Deprecated( + """use {@link #getPermissionState(String)} and {@link #getPermissionStates()} to get + the states of permissions defined on the Plugin in conjunction with the @CapacitorPlugin + annotation. Use the Android API {@link ActivityCompat#checkSelfPermission(Context, String)} + methods to check permissions with Android permission strings + + """ + ) + fun hasPermission(permission: String?): Boolean { + return ActivityCompat.checkSelfPermission( + context!!, + permission!! + ) == PackageManager.PERMISSION_GRANTED + } + + /** + * If the plugin annotation specified a set of permissions, this method checks if each is + * granted + * @return + */ + @Deprecated( + """use {@link #getPermissionState(String)} or {@link #getPermissionStates()} to + check whether permissions are granted or not + + """ + ) + fun hasRequiredPermissions(): Boolean { + val annotation = pluginHandle.getPluginAnnotation() + if (annotation == null) { + // Check for legacy plugin annotation, @NativePlugin + val legacyAnnotation = pluginHandle.getLegacyPluginAnnotation() + for (perm in legacyAnnotation!!.permissions) { + if (ActivityCompat.checkSelfPermission( + context!!, + perm!! + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + + return true + } + + for (perm in annotation.permissions) { + for (permString in perm.strings) { + if (ActivityCompat.checkSelfPermission( + context!!, + permString!! + ) != PackageManager.PERMISSION_GRANTED + ) { + return false + } + } + } + + return true + } + + /** + * Request all of the specified permissions in the CapacitorPlugin annotation (if any) + * + * If there is no registered permission callback for the PluginCall passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @since 3.0.0 + * @param call the plugin call + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestAllPermissions(call: PluginCall, callbackName: String) { + val annotation = pluginHandle.getPluginAnnotation() + if (annotation != null) { + val perms = HashSet() + for (perm in annotation.permissions) { + perms.addAll(Arrays.asList(*perm.strings)) + } + + permissionActivityResult(call, perms.toTypedArray(), callbackName) + } + } + + /** + * Request permissions using an alias defined on the plugin. + * + * If there is no registered permission callback for the PluginCall passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param alias an alias defined on the plugin + * @param call the plugin call involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestPermissionForAlias(alias: String, call: PluginCall, callbackName: String) { + requestPermissionForAliases(arrayOf(alias), call, callbackName) + } + + /** + * Request permissions using aliases defined on the plugin. + * + * If there is no registered permission callback for the PluginCall passed in, the call will + * be rejected. Make sure a valid permission callback method is registered using the + * [PermissionCallback] annotation. + * + * @param aliases a set of aliases defined on the plugin + * @param call the plugin call involved in originating the request + * @param callbackName the name of the callback to run when the permission request is complete + */ + protected fun requestPermissionForAliases( + aliases: Array, + call: PluginCall, + callbackName: String + ) { + if (aliases.size == 0) { + Logger.Companion.error("No permission alias was provided") + return + } + + val permissions = getPermissionStringsForAliases(aliases) + + if (permissions.size > 0) { + permissionActivityResult(call, permissions, callbackName) + } + } + + /** + * Gets the Android permission strings defined on the [CapacitorPlugin] annotation with + * the provided aliases. + * + * @param aliases aliases for permissions defined on the plugin + * @return Android permission strings associated with the provided aliases, if exists + */ + private fun getPermissionStringsForAliases(aliases: Array): Array { + val annotation = pluginHandle.getPluginAnnotation() + val perms = HashSet() + for (perm in annotation!!.permissions) { + if (Arrays.asList(*aliases).contains(perm.alias)) { + perms.addAll(Arrays.asList(*perm.strings)) + } + } + + return perms.toTypedArray() + } + + /** + * Gets the activity launcher associated with the calling methodName, or rejects the call if + * no registered launcher exists + * + * @param call the plugin call + * @param methodName the name of the activity callback method + * @return a launcher, or null if none found + */ + private fun getActivityLauncherOrReject( + call: PluginCall, + methodName: String + ): ActivityResultLauncher? { + val activityLauncher = activityLaunchers[methodName] + + // if there is no registered launcher, reject the call with an error and return null + if (activityLauncher == null) { + var registerError = + "There is no ActivityCallback method registered for the name: %s. " + + "Please define a callback method annotated with @ActivityCallback " + + "that receives arguments: (PluginCall, ActivityResult)" + registerError = String.format(Locale.US, registerError, methodName) + Logger.Companion.error(registerError) + call.reject(registerError) + return null + } + + return activityLauncher + } + + /** + * Gets the permission launcher associated with the calling methodName, or rejects the call if + * no registered launcher exists + * + * @param call the plugin call + * @param methodName the name of the permission callback method + * @return a launcher, or null if none found + */ + private fun getPermissionLauncherOrReject( + call: PluginCall, + methodName: String + ): ActivityResultLauncher>? { + val permissionLauncher = permissionLaunchers[methodName] + + // if there is no registered launcher, reject the call with an error and return null + if (permissionLauncher == null) { + var registerError = + "There is no PermissionCallback method registered for the name: %s. " + + "Please define a callback method annotated with @PermissionCallback " + + "that receives arguments: (PluginCall)" + registerError = String.format(Locale.US, registerError, methodName) + Logger.Companion.error(registerError) + call.reject(registerError) + return null + } + + return permissionLauncher + } + + /** + * Request all of the specified permissions in the CapacitorPlugin annotation (if any) + * + */ + @Deprecated("use {@link #requestAllPermissions(PluginCall, String)} in conjunction with @CapacitorPlugin") + fun pluginRequestAllPermissions() { + val legacyAnnotation = pluginHandle.getLegacyPluginAnnotation() + ActivityCompat.requestPermissions( + activity!!, + legacyAnnotation!!.permissions, + legacyAnnotation!!.permissionRequestCode + ) + } + + /** + * Helper for requesting a specific permission + * + * @param permission the permission to request + * @param requestCode the requestCode to use to associate the result with the plugin + */ + @Deprecated("use {@link #requestPermissionForAlias(String, PluginCall, String)} in conjunction with @CapacitorPlugin") + fun pluginRequestPermission(permission: String, requestCode: Int) { + ActivityCompat.requestPermissions(activity!!, arrayOf(permission), requestCode) + } + + /** + * Helper for requesting specific permissions + * @param permissions the set of permissions to request + * @param requestCode the requestCode to use to associate the result with the plugin + */ + @Deprecated( + """use {@link #requestPermissionForAliases(String[], PluginCall, String)} in conjunction + with @CapacitorPlugin + + """ + ) + fun pluginRequestPermissions(permissions: Array?, requestCode: Int) { + ActivityCompat.requestPermissions(activity!!, permissions!!, requestCode) + } + + /** + * Get the permission state for the provided permission alias. + * + * @param alias the permission alias to get + * @return the state of the provided permission alias or null + */ + fun getPermissionState(alias: String): PermissionState? { + return permissionStates[alias] + } + + val permissionStates: Map + /** + * Helper to check all permissions defined on a plugin and see the state of each. + * + * @since 3.0.0 + * @return A mapping of permission aliases to the associated granted status. + */ + get() = bridge!!.getPermissionStates(this) + + /** + * Add a listener for the given event + * @param eventName + * @param call + */ + private fun addEventListener(eventName: String?, call: PluginCall) { + var listeners = eventListeners[eventName] + if (listeners == null || listeners.isEmpty()) { + listeners = ArrayList() + eventListeners[eventName] = listeners + + // Must add the call before sending retained arguments + listeners.add(call) + + sendRetainedArgumentsForEvent(eventName) + } else { + listeners.add(call) + } + } + + /** + * Remove a listener from the given event + * @param eventName + * @param call + */ + private fun removeEventListener(eventName: String?, call: PluginCall) { + val listeners = eventListeners[eventName] ?: return + + listeners.remove(call) + } + + /** + * Notify all listeners that an event occurred + * @param eventName + * @param data + */ + /** + * Notify all listeners that an event occurred + * This calls [Plugin.notifyListeners] + * with retainUntilConsumed set to false + * @param eventName + * @param data + */ + protected fun notifyListeners( + eventName: String?, + data: JSObject, + retainUntilConsumed: Boolean = false + ) { + Logger.Companion.verbose(logTag, "Notifying listeners for event $eventName") + val listeners: List? = eventListeners[eventName] + if (listeners == null || listeners.isEmpty()) { + Logger.Companion.debug(logTag, "No listeners found for event $eventName") + if (retainUntilConsumed) { + var argList = retainedEventArguments[eventName] + + if (argList == null) { + argList = ArrayList() + } + + argList.add(data) + retainedEventArguments[eventName] = argList + } + return + } + + val listenersCopy: CopyOnWriteArrayList = CopyOnWriteArrayList(listeners) + for (call in listenersCopy) { + call!!.resolve(data) + } + } + + /** + * Check if there are any listeners for the given event + */ + protected fun hasListeners(eventName: String?): Boolean { + val listeners = eventListeners[eventName] + ?: return false + return !listeners.isEmpty() + } + + /** + * Send retained arguments (if any) for this event. This + * is called only when the first listener for an event is added + * @param eventName + */ + private fun sendRetainedArgumentsForEvent(eventName: String?) { + // copy retained args and null source to prevent potential race conditions + val retainedArgs = retainedEventArguments[eventName] + ?: return + + retainedEventArguments.remove(eventName) + + for (retained in retainedArgs) { + notifyListeners(eventName, retained) + } + } + + /** + * Exported plugin call for adding a listener to this plugin + * @param call + */ + @Suppress("unused") + @PluginMethod(returnType = PluginMethod.Companion.RETURN_NONE) + fun addListener(call: PluginCall) { + val eventName = call.getString("eventName") + call.setKeepAlive(true) + addEventListener(eventName, call) + } + + /** + * Exported plugin call to remove a listener from this plugin + * @param call + */ + @Suppress("unused") + @PluginMethod(returnType = PluginMethod.Companion.RETURN_NONE) + fun removeListener(call: PluginCall) { + val eventName = call.getString("eventName") + val callbackId = call.getString("callbackId") + val savedCall = bridge!!.getSavedCall(callbackId) + if (savedCall != null) { + removeEventListener(eventName, savedCall) + bridge!!.releaseCall(savedCall) + } + } + + /** + * Exported plugin call to remove all listeners from this plugin + * @param call + */ + @Suppress("unused") + @PluginMethod(returnType = PluginMethod.Companion.RETURN_PROMISE) + fun removeAllListeners(call: PluginCall) { + eventListeners.clear() + call.resolve() + } + + /** + * Exported plugin call for checking the granted status for each permission + * declared on the plugin. This plugin call responds with a mapping of permissions to + * the associated granted status. + * + * @since 3.0.0 + */ + @PluginMethod + @PermissionCallback + open fun checkPermissions(pluginCall: PluginCall) { + val permissionsResult = permissionStates + + if (permissionsResult.size == 0) { + // if no permissions are defined on the plugin, resolve undefined + pluginCall.resolve() + } else { + val permissionsResultJSON = JSObject() + for ((key, value) in permissionsResult) { + permissionsResultJSON.put(key, value) + } + + pluginCall.resolve(permissionsResultJSON) + } + } + + /** + * Exported plugin call to request all permissions for this plugin. + * To manually request permissions within a plugin use: + * [.requestAllPermissions], or + * [.requestPermissionForAlias], or + * [.requestPermissionForAliases] + * + * @param call the plugin call + */ + @PluginMethod + open fun requestPermissions(call: PluginCall) { + val annotation = pluginHandle.getPluginAnnotation() + if (annotation == null) { + handleLegacyPermission(call) + } else { + // handle permission requests for plugins defined with @CapacitorPlugin (since 3.0.0) + var permAliases: Array? = null + val autoGrantPerms: MutableSet = HashSet() + + // If call was made with a list of specific permission aliases to request, save them + // to be requested + val providedPerms = call.getArray("permissions") + var providedPermsList: List? = null + + if (providedPerms != null) { + try { + providedPermsList = providedPerms.toList() + } catch (ignore: JSONException) { + // do nothing + } + } + + // If call was made without any custom permissions, request all from plugin annotation + val aliasSet: MutableSet = HashSet() + if (providedPermsList == null || providedPermsList.isEmpty()) { + for (perm in annotation.permissions) { + // If a permission is defined with no permission strings, separate it for auto-granting. + // Otherwise, the alias is added to the list to be requested. + if (perm.strings.size == 0 || (perm.strings.size == 1 && perm.strings[0].isEmpty())) { + if (!perm.alias.isEmpty()) { + autoGrantPerms.add(perm.alias) + } + } else { + aliasSet.add(perm.alias) + } + } + + permAliases = aliasSet.toTypedArray() + } else { + for (perm in annotation.permissions) { + if (providedPermsList.contains(perm.alias)) { + aliasSet.add(perm.alias) + } + } + + if (aliasSet.isEmpty()) { + call.reject("No valid permission alias was requested of this plugin.") + } else { + permAliases = aliasSet.toTypedArray() + } + } + + if (permAliases != null && permAliases.size > 0) { + // request permissions using provided aliases or all defined on the plugin + requestPermissionForAliases(permAliases, call, "checkPermissions") + } else if (!autoGrantPerms.isEmpty()) { + // if the plugin only has auto-grant permissions, return all as GRANTED + val permissionsResults = JSObject() + + for (perm in autoGrantPerms) { + permissionsResults.put(perm, PermissionState.GRANTED.toString()) + } + + call.resolve(permissionsResults) + } else { + // no permissions are defined on the plugin, resolve undefined + call.resolve() + } + } + } + + @Suppress("deprecation") + private fun handleLegacyPermission(call: PluginCall) { + // handle permission requests for plugins defined with @NativePlugin (prior to 3.0.0) + val legacyAnnotation = pluginHandle.getLegacyPluginAnnotation() + val perms = legacyAnnotation!!.permissions + if (perms.size > 0) { + saveCall(call) + pluginRequestPermissions(perms, legacyAnnotation!!.permissionRequestCode) + } else { + call.resolve() + } + } + + /** + * Handle request permissions result. A plugin using the deprecated [NativePlugin] + * should override this to handle the result, or this method will handle the result + * for our convenient requestPermissions call. + * @param requestCode + * @param permissions + * @param grantResults + */ + @Deprecated( + """in favor of using callbacks in conjunction with {@link CapacitorPlugin} + + """ + ) + protected fun handleRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray? + ) { + if (!hasDefinedPermissions(permissions)) { + val builder = StringBuilder() + builder.append("Missing the following permissions in AndroidManifest.xml:\n") + val missing = PermissionHelper.getUndefinedPermissions( + context, permissions + ) + for (perm in missing) { + builder.append(perm + "\n") + } + savedCall!!.reject(builder.toString()) + savedCall = null + } + } + + /** + * Called before the app is destroyed to give a plugin the chance to + * save the last call options for a saved plugin. By default, this + * method saves the full JSON blob of the options call. Since Bundle sizes + * may be limited, plugins that expect to be called with large data + * objects (such as a file), should override this method and selectively + * store option values in a [Bundle] to avoid exceeding limits. + * @return a new [Bundle] with fields set from the options of the last saved [PluginCall] + */ + protected fun saveInstanceState(): Bundle? { + val savedCall = bridge!!.getSavedCall(lastPluginCallId) ?: return null + + val ret = Bundle() + val callData = savedCall.data + + if (callData != null) { + ret.putString(BUNDLE_PERSISTED_OPTIONS_JSON_KEY, callData.toString()) + } + + return ret + } + + /** + * Called when the app is opened with a previously un-handled + * activity response. If the plugin that started the activity + * stored data in [Plugin.saveInstanceState] then this + * method will be called to allow the plugin to restore from that. + * @param state + */ + protected fun restoreState(state: Bundle?) {} + + /** + * Handle activity result, should be overridden by each plugin + * + * @param requestCode + * @param resultCode + * @param data + */ + @Deprecated( + """provide a callback method using the {@link ActivityCallback} annotation and use + the {@link #startActivityForResult(PluginCall, Intent, String)} method + + """ + ) + protected fun handleOnActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + } + + /** + * Handle onNewIntent + * @param intent + */ + protected open fun handleOnNewIntent(intent: Intent?) {} + + /** + * Handle onConfigurationChanged + * @param newConfig + */ + protected fun handleOnConfigurationChanged(newConfig: Configuration?) {} + + /** + * Handle onStart + */ + protected fun handleOnStart() {} + + /** + * Handle onRestart + */ + protected fun handleOnRestart() {} + + /** + * Handle onResume + */ + protected open fun handleOnResume() {} + + /** + * Handle onPause + */ + protected open fun handleOnPause() {} + + /** + * Handle onStop + */ + protected open fun handleOnStop() {} + + /** + * Handle onDestroy + */ + protected open fun handleOnDestroy() {} + + /** + * Give the plugins a chance to take control when a URL is about to be loaded in the WebView. + * Returning true causes the WebView to abort loading the URL. + * Returning false causes the WebView to continue loading the URL. + * Returning null will defer to the default Capacitor policy + */ + @Suppress("unused") + fun shouldOverrideLoad(url: Uri?): Boolean? { + return null + } + + /** + * Start a new Activity. + * + * Note: This method must be used by all plugins instead of calling + * [Activity.startActivityForResult] as it associates the plugin with + * any resulting data from the new Activity even if this app + * is destroyed by the OS (to free up memory, for example). + * @param intent + * @param resultCode + */ + @Deprecated("") + protected fun startActivityForResult(call: PluginCall?, intent: Intent?, resultCode: Int) { + bridge!!.startActivityForPluginWithResult(call, intent, resultCode) + } + + /** + * Execute the given runnable on the Bridge's task handler + * @param runnable + */ + fun execute(runnable: Runnable?) { + bridge!!.execute(runnable) + } + + /** + * Shortcut for getting the plugin log tag + * @param subTags + */ + protected fun getLogTag(vararg subTags: String?): String { + return Logger.Companion.tags(*subTags) + } + + protected val logTag: String + /** + * Gets a plugin log tag with the child's class name as subTag. + */ + get() = Logger.Companion.tags(this.javaClass.simpleName) + + companion object { + // The key we will use inside of a persisted Bundle for the JSON blob + // for a plugin call options. + private const val BUNDLE_PERSISTED_OPTIONS_JSON_KEY = "_json" + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java deleted file mode 100644 index 18661d76..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.java +++ /dev/null @@ -1,440 +0,0 @@ -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 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); - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.kt new file mode 100644 index 00000000..6e63b5b6 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginCall.kt @@ -0,0 +1,333 @@ +package com.getcapacitor + +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject + +/** + * Wraps a call from the web layer to native + */ +class PluginCall( + private val msgHandler: MessageHandler, + val pluginId: String?, + val callbackId: String?, + @JvmField val methodName: String?, + @JvmField val data: JSObject? +) { + /** + * Gets the keepAlive value of the plugin call + * @return true if the plugin call is kept alive + */ + var isKeptAlive: Boolean = false + private set + + /** + * Indicates that this PluginCall was released, and should no longer be used + */ + @get:Deprecated("") + @Deprecated("") + var isReleased: Boolean = false + private set + + fun successCallback(successResult: PluginResult?) { + if (CALLBACK_ID_DANGLING == this.callbackId) { + // don't send back response if the callbackId was "-1" + return + } + + msgHandler.sendResponseMessage(this, successResult, null) + } + + + @Deprecated(" Use {@link #resolve(JSObject data)}") + fun success(data: JSObject) { + val result = PluginResult(data) + msgHandler.sendResponseMessage(this, result, null) + } + + + @Deprecated(" Use {@link #resolve()}") + fun success() { + this.resolve(JSObject()) + } + + fun resolve(data: JSObject) { + val result = PluginResult(data) + msgHandler.sendResponseMessage(this, result, null) + } + + fun resolve() { + msgHandler.sendResponseMessage(this, null, null) + } + + fun errorCallback(msg: String?) { + val errorResult = PluginResult() + + try { + errorResult.put("message", msg) + } catch (jsonEx: Exception) { + Logger.Companion.error(Logger.Companion.tags("Plugin"), jsonEx.toString(), null) + } + + msgHandler.sendResponseMessage(this, null, errorResult) + } + + + @Deprecated(" Use {@link #reject(String msg, Exception ex)}") + fun error(msg: String?, ex: Exception?) { + reject(msg, ex) + } + + + @Deprecated(" Use {@link #reject(String msg, String code, Exception ex)}") + fun error(msg: String?, code: String?, ex: Exception?) { + reject(msg, code, ex) + } + + + @Deprecated(" Use {@link #reject(String msg)}") + fun error(msg: String?) { + reject(msg) + } + + @JvmOverloads + fun reject(msg: String?, code: String? = null, ex: Exception? = null, data: JSObject? = null) { + val errorResult = PluginResult() + + if (ex != null) { + Logger.Companion.error(Logger.Companion.tags("Plugin"), msg, ex) + } + + try { + errorResult.put("message", msg) + errorResult.put("code", code) + if (null != data) { + errorResult.put("data", data) + } + } catch (jsonEx: Exception) { + Logger.Companion.error(Logger.Companion.tags("Plugin"), jsonEx.message, jsonEx) + } + + msgHandler.sendResponseMessage(this, null, errorResult) + } + + fun reject(msg: String?, ex: Exception?, data: JSObject?) { + reject(msg, null, ex, data) + } + + fun reject(msg: String?, code: String?, data: JSObject?) { + reject(msg, code, null, data) + } + + fun reject(msg: String?, data: JSObject?) { + reject(msg, null, null, data) + } + + fun reject(msg: String?, ex: Exception?) { + reject(msg, null, ex, null) + } + + @JvmOverloads + fun unimplemented(msg: String? = "not implemented") { + reject(msg, "UNIMPLEMENTED", null, null) + } + + @JvmOverloads + fun unavailable(msg: String? = "not available") { + reject(msg, "UNAVAILABLE", null, null) + } + + fun getString(name: String?): String? { + return this.getString(name, null) + } + + fun getString(name: String?, defaultValue: String?): String? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is String) { + return value + } + return defaultValue + } + + fun getInt(name: String?): Int? { + return this.getInt(name, null) + } + + fun getInt(name: String?, defaultValue: Int?): Int? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is Int) { + return value + } + return defaultValue + } + + fun getLong(name: String?): Long? { + return this.getLong(name, null) + } + + fun getLong(name: String?, defaultValue: Long?): Long? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is Long) { + return value + } + return defaultValue + } + + fun getFloat(name: String?): Float? { + return this.getFloat(name, null) + } + + fun getFloat(name: String?, defaultValue: Float?): Float? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is Float) { + return value + } + if (value is Double) { + return value.toFloat() + } + if (value is Int) { + return value.toFloat() + } + return defaultValue + } + + fun getDouble(name: String?): Double? { + return this.getDouble(name, null) + } + + fun getDouble(name: String?, defaultValue: Double?): Double? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is Double) { + return value + } + if (value is Float) { + return value.toDouble() + } + if (value is Int) { + return value.toDouble() + } + return defaultValue + } + + fun getBoolean(name: String?): Boolean? { + return this.getBoolean(name, null) + } + + fun getBoolean(name: String?, defaultValue: Boolean?): Boolean? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is Boolean) { + return value + } + return defaultValue + } + + fun getObject(name: String?): JSObject? { + return this.getObject(name, null) + } + + fun getObject(name: String?, defaultValue: JSObject?): JSObject? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is JSONObject) { + return try { + JSObject.Companion.fromJSONObject(value) + } catch (ex: JSONException) { + defaultValue + } + } + return defaultValue + } + + fun getArray(name: String?): JSArray? { + return this.getArray(name, null) + } + + /** + * Get a JSONArray and turn it into a JSArray + * @param name + * @param defaultValue + * @return + */ + fun getArray(name: String?, defaultValue: JSArray?): JSArray? { + val value = data!!.opt(name) ?: return defaultValue + + if (value is JSONArray) { + try { + val valueArray = value + val items: MutableList = ArrayList() + for (i in 0 until valueArray.length()) { + items.add(valueArray[i]) + } + return JSArray(items.toTypedArray()) + } catch (ex: JSONException) { + 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.""" + ) + fun hasOption(name: String?): Boolean { + return 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") + fun 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 + */ + fun setKeepAlive(keepAlive: Boolean) { + this.isKeptAlive = keepAlive + } + + fun release(bridge: Bridge?) { + this.isKeptAlive = false + bridge!!.releaseCall(this) + this.isReleased = true + } + + @get:Deprecated( + """use {@link #isKeptAlive()} + """ + ) + val isSaved: Boolean + /** + * @return true if the plugin call is kept alive + */ + get() = isKeptAlive + + internal inner class PluginCallDataTypeException(m: String?) : Exception(m) + companion object { + /** + * 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. + */ + const val CALLBACK_ID_DANGLING: String = "-1" + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.kt similarity index 60% rename from @capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java rename to @capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.kt index 0f00fc53..56625a1b 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginConfig.kt @@ -1,26 +1,28 @@ -package com.getcapacitor; +package com.getcapacitor -import com.getcapacitor.util.JSONUtils; -import org.json.JSONObject; +import com.getcapacitor.Bridge.config +import com.getcapacitor.util.JSONUtils +import org.json.JSONObject /** * Represents the configuration options for plugins used by Capacitor */ -public class PluginConfig { - +class PluginConfig +/** + * Constructs a PluginsConfig with the provided JSONObject value. + * + * @param config A plugin configuration expressed as a JSON Object + */ internal constructor( /** * The object containing plugin config values. */ - private final JSONObject config; - + val configJSON: JSONObject? +) { /** - * Constructs a PluginsConfig with the provided JSONObject value. + * Gets the JSON Object containing the config of the the provided plugin ID. * - * @param config A plugin configuration expressed as a JSON Object + * @return The config for that plugin */ - PluginConfig(JSONObject config) { - this.config = config; - } /** * Get a string value for a plugin in the Capacitor config. @@ -28,8 +30,8 @@ public class PluginConfig { * @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); + fun getString(configKey: String?): String { + return getString(configKey, null) } /** @@ -39,8 +41,8 @@ public String getString(String configKey) { * @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); + fun getString(configKey: String?, defaultValue: String?): String { + return JSONUtils.getString(configJSON, configKey, defaultValue) } /** @@ -50,8 +52,8 @@ public String getString(String configKey, String defaultValue) { * @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); + fun getBoolean(configKey: String?, defaultValue: Boolean): Boolean { + return JSONUtils.getBoolean(configJSON, configKey, defaultValue) } /** @@ -61,8 +63,8 @@ public boolean getBoolean(String configKey, boolean defaultValue) { * @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); + fun getInt(configKey: String?, defaultValue: Int): Int { + return JSONUtils.getInt(configJSON, configKey, defaultValue) } /** @@ -71,8 +73,8 @@ public int getInt(String configKey, int defaultValue) { * @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); + fun getArray(configKey: String?): Array { + return getArray(configKey, null) } /** @@ -82,8 +84,8 @@ public String[] getArray(String configKey) { * @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); + fun getArray(configKey: String?, defaultValue: Array?): Array { + return JSONUtils.getArray(configJSON, configKey, defaultValue) } /** @@ -92,25 +94,15 @@ public String[] getArray(String configKey, String[] defaultValue) { * @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); + fun getObject(configKey: String?): JSONObject { + return JSONUtils.getObject(configJSON, 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; - } + val isEmpty: Boolean + /** + * Check if the PluginConfig is empty. + * + * @return true if the plugin config has no entries + */ + get() = configJSON!!.length() == 0 } diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java deleted file mode 100644 index 2e520b3a..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.java +++ /dev/null @@ -1,160 +0,0 @@ -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 pluginClass; - - private final Map pluginMethods = new HashMap<>(); - - private final String pluginId; - - @SuppressWarnings("deprecation") - private NativePlugin legacyPluginAnnotation; - - private CapacitorPlugin pluginAnnotation; - - private Plugin instance; - - @SuppressWarnings("deprecation") - private PluginHandle(Class 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 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 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 getMethods() { - return this.pluginMethods.values(); - } - - public Plugin load() throws PluginLoadException { - if (this.instance != null) { - return this.instance; - } - - try { - this.instance = this.pluginClass.newInstance(); - return this.loadInstance(instance); - } catch (InstantiationException | IllegalAccessException 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 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); - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.kt new file mode 100644 index 00000000..c12e360b --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginHandle.kt @@ -0,0 +1,137 @@ +package com.getcapacitor + +import com.getcapacitor.InvalidPluginMethodException +import com.getcapacitor.PluginLoadException +import com.getcapacitor.annotation.CapacitorPlugin +import java.lang.reflect.InvocationTargetException + +/** + * PluginHandle is an instance of a plugin that has been registered + * and indexed. Think of it as a Plugin instance with extra metadata goodies + */ +class PluginHandle @Suppress("deprecation") private constructor( + val pluginClass: Class, + private val bridge: Bridge +) { + private val pluginMethods: MutableMap = HashMap() + + var id: String? = null + + @get:Suppress("deprecation") + @Suppress("deprecation") + var legacyPluginAnnotation: NativePlugin? = null + + var pluginAnnotation: CapacitorPlugin? = null + + var instance: Plugin? = null + private set + + init { + val pluginAnnotation = pluginClass.getAnnotation( + CapacitorPlugin::class.java + ) + if (pluginAnnotation == null) { + // Check for legacy plugin annotation, @NativePlugin + val legacyPluginAnnotation = pluginClass.getAnnotation( + NativePlugin::class.java + ) + if (legacyPluginAnnotation == null) { + throw InvalidPluginException("No @CapacitorPlugin annotation found for plugin " + pluginClass.name) + } + + if (legacyPluginAnnotation.name != "") { + this.id = legacyPluginAnnotation.name + } else { + this.id = pluginClass.simpleName + } + + this.legacyPluginAnnotation = legacyPluginAnnotation + } else { + if (pluginAnnotation.name != "") { + this.id = pluginAnnotation.name + } else { + this.id = pluginClass.simpleName + } + + this.pluginAnnotation = pluginAnnotation + } + + this.indexMethods(pluginClass) + } + + constructor(bridge: Bridge, pluginClass: Class) : this(pluginClass, bridge) { + this.load() + } + + constructor(bridge: Bridge, plugin: Plugin) : this(plugin.javaClass, bridge) { + this.loadInstance(plugin) + } + + val methods: Collection + get() = pluginMethods.values + + @Throws(PluginLoadException::class) + fun load(): Plugin? { + if (this.instance != null) { + return this.instance + } + + try { + this.instance = pluginClass.newInstance() + return this.loadInstance(instance) + } catch (ex: InstantiationException) { + throw PluginLoadException("Unable to load plugin instance. Ensure plugin is publicly accessible") + } catch (ex: IllegalAccessException) { + throw PluginLoadException("Unable to load plugin instance. Ensure plugin is publicly accessible") + } + } + + fun loadInstance(plugin: Plugin?): Plugin? { + this.instance = plugin + instance.setPluginHandle(this) + instance.setBridge(this.bridge) + instance!!.load() + 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 + */ + @Throws( + PluginLoadException::class, + InvalidPluginMethodException::class, + InvocationTargetException::class, + IllegalAccessException::class + ) + fun invoke(methodName: String, call: PluginCall?) { + if (this.instance == null) { + // Can throw PluginLoadException + this.load() + } + + val methodMeta = pluginMethods[methodName] + ?: throw InvalidPluginMethodException("No method " + methodName + " found for plugin " + pluginClass.name) + + methodMeta.method.invoke(this.instance, call) + } + + /** + * Index all the known callable methods for a plugin for faster + * invocation later + */ + private fun indexMethods(plugin: Class) { + //Method[] methods = pluginClass.getDeclaredMethods(); + val methods = pluginClass.methods + + for (methodReflect in methods) { + val method = methodReflect.getAnnotation(PluginMethod::class.java) ?: continue + + val methodMeta = PluginMethodHandle(methodReflect, method) + pluginMethods[methodReflect.name] = methodMeta + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java deleted file mode 100644 index ae6b0eb8..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.java +++ /dev/null @@ -1,16 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.kt new file mode 100644 index 00000000..7ea5c2b2 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginInvocationException.kt @@ -0,0 +1,9 @@ +package com.getcapacitor + +internal class PluginInvocationException : Exception { + constructor(s: String?) : super(s) + + constructor(t: Throwable?) : super(t) + + constructor(s: String?, t: Throwable?) : super(s, t) +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java deleted file mode 100644 index 8d81a382..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.java +++ /dev/null @@ -1,19 +0,0 @@ -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); - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.kt new file mode 100644 index 00000000..5223697a --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginLoadException.kt @@ -0,0 +1,12 @@ +package com.getcapacitor + +/** + * Thrown when a plugin fails to instantiate + */ +class PluginLoadException : Exception { + constructor(s: String?) : super(s) + + constructor(t: Throwable?) : super(t) + + constructor(s: String?, t: Throwable?) : super(s, t) +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java deleted file mode 100644 index 540bc912..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.java +++ /dev/null @@ -1,56 +0,0 @@ -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> loadPluginClasses() throws PluginLoadException { - JSONArray pluginsJSON = parsePluginsJSON(); - ArrayList> 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"); - } - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.kt new file mode 100644 index 00000000..f88b56e0 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginManager.kt @@ -0,0 +1,54 @@ +package com.getcapacitor + +import android.content.res.AssetManager +import com.getcapacitor.PluginLoadException +import org.json.JSONArray +import org.json.JSONException +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader + +class PluginManager(private val assetManager: AssetManager) { + @Throws(PluginLoadException::class) + fun loadPluginClasses(): List> { + val pluginsJSON = parsePluginsJSON() + val pluginList = ArrayList>() + + try { + var i = 0 + val size = pluginsJSON.length() + while (i < size) { + val pluginJSON = pluginsJSON.getJSONObject(i) + val classPath = pluginJSON.getString("classpath") + val c = Class.forName(classPath) + pluginList.add(c.asSubclass(Plugin::class.java)) + i++ + } + } catch (e: JSONException) { + throw PluginLoadException("Could not parse capacitor.plugins.json as JSON") + } catch (e: ClassNotFoundException) { + throw PluginLoadException("Could not find class by class path: " + e.message) + } + + return pluginList + } + + @Throws(PluginLoadException::class) + private fun parsePluginsJSON(): JSONArray { + try { + BufferedReader(InputStreamReader(assetManager.open("capacitor.plugins.json"))).use { reader -> + val builder = StringBuilder() + var line: String? + while ((reader.readLine().also { line = it }) != null) { + builder.append(line) + } + val jsonString = builder.toString() + return JSONArray(jsonString) + } + } catch (e: IOException) { + throw PluginLoadException("Could not load capacitor.plugins.json") + } catch (e: JSONException) { + throw PluginLoadException("Could not parse capacitor.plugins.json as JSON") + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.java deleted file mode 100644 index 85663043..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.java +++ /dev/null @@ -1,15 +0,0 @@ -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; -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.kt new file mode 100644 index 00000000..13a9e8f3 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethod.kt @@ -0,0 +1,12 @@ +package com.getcapacitor + +@Retention(AnnotationRetention.RUNTIME) +annotation class PluginMethod(val returnType: String = RETURN_PROMISE) { + companion object { + const val RETURN_PROMISE: String = "promise" + + const val RETURN_CALLBACK: String = "callback" + + const val RETURN_NONE: String = "none" + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java deleted file mode 100644 index a728c1f1..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.java +++ /dev/null @@ -1,33 +0,0 @@ -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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.kt new file mode 100644 index 00000000..85d96dd7 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginMethodHandle.kt @@ -0,0 +1,15 @@ +package com.getcapacitor + +import java.lang.reflect.Method + +class PluginMethodHandle(// The reflect method reference + val method: Method, methodDecorator: PluginMethod +) { + // The name of the method + + val name: String = method.name + + // The return type of the method (see PluginMethod for constants) + + val returnType: String = methodDecorator.returnType +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.java deleted file mode 100644 index cdc169e0..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.java +++ /dev/null @@ -1,84 +0,0 @@ -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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.kt new file mode 100644 index 00000000..fc044314 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/PluginResult.kt @@ -0,0 +1,74 @@ +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. + */ +class PluginResult @JvmOverloads constructor(private val json: JSObject = JSObject()) { + fun put(name: String, value: Boolean): PluginResult { + return this.jsonPut(name, value) + } + + fun put(name: String, value: Double): PluginResult { + return this.jsonPut(name, value) + } + + fun put(name: String, value: Int): PluginResult { + return this.jsonPut(name, value) + } + + fun put(name: String, value: Long): PluginResult { + return this.jsonPut(name, value) + } + + /** + * Format a date as an ISO string + */ + fun put(name: String, value: Date?): PluginResult { + val tz = TimeZone.getTimeZone("UTC") + val df: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") + df.timeZone = tz + return this.jsonPut(name, df.format(value)) + } + + fun put(name: String, value: Any?): PluginResult { + return this.jsonPut(name, value!!) + } + + fun put(name: String, value: PluginResult?): PluginResult { + return this.jsonPut(name, value!!.json) + } + + fun jsonPut(name: String, value: Any): PluginResult { + try { + json.put(name, value) + } catch (ex: Exception) { + Logger.Companion.error(Logger.Companion.tags("Plugin"), "", ex) + } + return this + } + + override fun toString(): String { + return json.toString() + } + + val wrappedResult: JSObject + /** + * 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. + */ + get() { + val ret = JSObject() + ret.put("pluginId", json.getString("pluginId")) + ret.put("methodName", json.getString("methodName")) + ret.put("success", json.getBoolean("success", false)!!) + ret.put("data", json.getJSObject("data")!!) + ret.put("error", json.getJSObject("error")!!) + return ret + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java deleted file mode 100644 index eb3d7b0d..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.java +++ /dev/null @@ -1,37 +0,0 @@ -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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.kt new file mode 100644 index 00000000..5bc6f25b --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ProcessedRoute.kt @@ -0,0 +1,12 @@ +package com.getcapacitor + +/** + * An data class used in conjunction with RouteProcessor. + * + * @see com.getcapacitor.RouteProcessor + */ +class ProcessedRoute { + var path: String? = null + var isAsset: Boolean = false + var isIgnoreAssetPath: Boolean = false +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java deleted file mode 100644 index 670c8bc6..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.getcapacitor; - -/** - * An interface used in the processing of routes - */ -public interface RouteProcessor { - ProcessedRoute process(String basePath, String path); -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.kt new file mode 100644 index 00000000..6987c0a0 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/RouteProcessor.kt @@ -0,0 +1,8 @@ +package com.getcapacitor + +/** + * An interface used in the processing of routes + */ +interface RouteProcessor { + fun process(basePath: String?, path: String?): ProcessedRoute? +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.java deleted file mode 100644 index 5b34b460..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.java +++ /dev/null @@ -1,25 +0,0 @@ -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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.kt new file mode 100644 index 00000000..d4ec9e72 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/ServerPath.kt @@ -0,0 +1,8 @@ +package com.getcapacitor + +class ServerPath(val type: PathType, val path: String) { + enum class PathType { + BASE_PATH, + ASSET_PATH + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java deleted file mode 100644 index 715a0a0b..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.java +++ /dev/null @@ -1,180 +0,0 @@ -/* - * 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. - *

- * 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 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 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 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 mChildren; -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.kt new file mode 100644 index 00000000..94a5dc2c --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/UriMatcher.kt @@ -0,0 +1,169 @@ +/* + * 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.regex.Pattern + +class UriMatcher { + /** + * Creates the root node of the URI tree. + * + * @param code the code to match for the root URI + */ + constructor(code: Any?) { + mCode = code + mWhich = -1 + mChildren = ArrayList() + mText = null + } + + private constructor() { + mCode = null + mWhich = -1 + mChildren = 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. + * + * + * Starting from API level [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. + */ + fun addURI(scheme: String?, authority: String?, path: String?, code: Any?) { + requireNotNull(code) { "Code can't be null" } + + var tokens: Array? = null + if (path != null) { + var newPath = path + // Strip leading slash if present. + if (!path.isEmpty() && path[0] == '/') { + newPath = path.substring(1) + } + tokens = PATH_SPLIT_PATTERN.split(newPath) + } + + val numTokens = tokens?.size ?: 0 + var node = this + for (i in -2 until numTokens) { + var token = if (i == -2) scheme else if (i == -1) authority else tokens!![i] + val children = node.mChildren + val numChildren = children.size + var child: UriMatcher + var j = 0 + while (j < numChildren) { + child = children[j] + if (token == child.mText) { + node = child + break + } + j++ + } + if (j == numChildren) { + // Child not found, create it + child = UriMatcher() + if (i == -1 && token!!.contains("*")) { + child.mWhich = MASK + } else if (token == "**") { + child.mWhich = REST + } else if (token == "*") { + child.mWhich = TEXT + } else { + child.mWhich = EXACT + } + child.mText = token + node.mChildren.add(child) + node = child + } + } + node.mCode = code + } + + /** + * 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. + */ + fun match(uri: Uri): Any? { + val pathSegments = uri.pathSegments + val li = pathSegments.size + + var node: UriMatcher? = this + + if (li == 0 && uri.authority == null) { + return this.mCode + } + + for (i in -2 until li) { + var u = if (i == -2) uri.scheme else if (i == -1) uri.authority else pathSegments[i] + val list = node!!.mChildren ?: break + node = null + val lj = list.size + for (j in 0 until lj) { + val n = list[j] + which_switch@ when (n.mWhich) { + MASK -> if (HostMask.Parser.parse(n.mText).matches(u)) { + node = n + } + + EXACT -> if (n.mText == u) { + node = n + } + + TEXT -> node = n + REST -> return n.mCode + } + if (node != null) { + break + } + } + if (node == null) { + return null + } + } + + return node!!.mCode + } + + private var mCode: Any? + private var mWhich: Int + private var mText: String? + private var mChildren: ArrayList + + companion object { + val PATH_SPLIT_PATTERN: Pattern = Pattern.compile("/") + + private const val EXACT = 0 + private const val TEXT = 1 + private const val REST = 2 + private const val MASK = 3 + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.kt similarity index 67% rename from @capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java rename to @capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.kt index 6df4f6c0..dbd5578b 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewListener.kt @@ -1,19 +1,18 @@ -package com.getcapacitor; +package com.getcapacitor -import android.webkit.RenderProcessGoneDetail; -import android.webkit.WebView; +import android.webkit.RenderProcessGoneDetail +import android.webkit.WebView /** - * Provides callbacks associated with the {@link BridgeWebViewClient} + * Provides callbacks associated with the [BridgeWebViewClient] */ -public abstract class WebViewListener { - +abstract class WebViewListener { /** * Callback for page load event. * * @param webView The WebView that loaded */ - public void onPageLoaded(WebView webView) { + fun onPageLoaded(webView: WebView?) { // Override me to add behavior to the page loaded event } @@ -22,7 +21,7 @@ public void onPageLoaded(WebView webView) { * * @param webView The WebView that loaded */ - public void onReceivedError(WebView webView) { + fun onReceivedError(webView: WebView?) { // Override me to add behavior to handle the onReceivedError event } @@ -31,7 +30,7 @@ public void onReceivedError(WebView webView) { * * @param webView The WebView that loaded */ - public void onReceivedHttpError(WebView webView) { + fun onReceivedHttpError(webView: WebView?) { // Override me to add behavior to handle the onReceivedHttpError event } @@ -40,7 +39,7 @@ public void onReceivedHttpError(WebView webView) { * * @param webView The WebView that loaded */ - public void onPageStarted(WebView webView) { + fun onPageStarted(webView: WebView?) { // Override me to add behavior to the page started event } @@ -50,8 +49,8 @@ public void onPageStarted(WebView webView) { * @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) { + fun onRenderProcessGone(webView: WebView?, detail: RenderProcessGoneDetail?): Boolean { // Override me to add behavior to the web view render process gone event - return false; + return false } } diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java deleted file mode 100644 index 3745a8f3..00000000 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.java +++ /dev/null @@ -1,878 +0,0 @@ -/* -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.net.URLDecoder; -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; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; - -import java.lang.reflect.Field; - -/** - * 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. - *

- * 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 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. - *

- * Methods of this handler will be invoked on a background thread and care must - * be taken to - * correctly synchronize access to any shared state. - *

- * 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 responseHeaders; - - public PathHandler() { - this(null, null, 200, "OK", null); - } - - public PathHandler(String encoding, String charset, int statusCode, String reasonPhrase, - Map responseHeaders) { - this.encoding = encoding; - this.charset = charset; - this.statusCode = statusCode; - this.reasonPhrase = reasonPhrase; - Map 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 getResponseHeaders() { - return responseHeaders; - } - } - - WebViewLocalServer(Context context, Bridge bridge, JSInjector jsInjector, ArrayList 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 - * request. - * 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 (loadingUrl.toString().endsWith("#image")) { - Map headers = new HashMap<>(request.getRequestHeaders()); - headers.remove("x-requested-with"); - - try { - URL url = new URL(loadingUrl.toString()); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - for (Map.Entry entry : headers.entrySet()) { - connection.setRequestProperty(entry.getKey(), entry.getValue()); - } - - String contentType = connection.getContentType(); - if (contentType == null) { - contentType = "image/unknown"; - } - - InputStream inputStream = connection.getInputStream(); - - return new WebResourceResponse(contentType, "UTF-8", inputStream); - - } catch (Exception e) { - e.printStackTrace(); - } - } - if (loadingUrl.toString().endsWith("#resolve")) { - Map headers = new HashMap<>(request.getRequestHeaders()); - headers.remove("x-requested-with"); - - try { - URL url = new URL(loadingUrl.toString()); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - for (Map.Entry entry : headers.entrySet()) { - connection.setRequestProperty(entry.getKey(), entry.getValue()); - } - - connection.setInstanceFollowRedirects(false); - connection.connect(); - - String locationHeader = connection.getHeaderField("Location"); - String resolvedUrl = (locationHeader != null) ? locationHeader : loadingUrl.toString(); - - WebResourceResponse response = new WebResourceResponse("text/plain", "UTF-8", - new ByteArrayInputStream(resolvedUrl.getBytes(StandardCharsets.UTF_8))); - - Map responseHeaders = new HashMap<>(); - for (Map.Entry> entry : connection.getHeaderFields().entrySet()) { - if (entry.getKey() != null && entry.getValue() != null && !entry.getValue().isEmpty()) { - responseHeaders.put(entry.getKey(), entry.getValue().get(0)); - } - } - - responseHeaders.put("x-location", resolvedUrl); - responseHeaders.put("Access-Control-Allow-Origin", "*"); - responseHeaders.put("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); - response.setResponseHeaders(responseHeaders); - - return response; - - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (loadingUrl.toString().endsWith("#animevsub-vsub_extra")) { - Map headers = new HashMap<>(request.getRequestHeaders()); - headers.remove("x-requested-with"); - headers.put("referer", "https://animevietsub.tv"); - - try { - URL url = new URL(loadingUrl.toString()); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - - for (Map.Entry entry : headers.entrySet()) { - connection.setRequestProperty(entry.getKey(), entry.getValue()); - } - - connection.setInstanceFollowRedirects(false); - connection.connect(); - - String locationHeader = connection.getHeaderField("Location"); - String resolvedUrl = (locationHeader != null) ? locationHeader : loadingUrl.toString(); - - Map responseHeaders = new HashMap<>(); - // for (Map.Entry> entry : - // connection.getHeaderFields().entrySet()) { - // if (entry.getKey() != null && entry.getValue() != null && - // !entry.getValue().isEmpty()) { - // responseHeaders.put(entry.getKey(), entry.getValue().get(0)); - // } - // } - // - responseHeaders.put("Access-Control-Allow-Origin", "*"); - responseHeaders.put("Access-Control-Allow-Methods", "PUT, GET, HEAD, POST, DELETE, OPTIONS"); - - // responseHeaders.put("Location", resolvedUrl); - responseHeaders.put("W-Location", resolvedUrl); - - WebResourceResponse response = new WebResourceResponse("text/plain", "UTF-8", - new ByteArrayInputStream(resolvedUrl.getBytes(StandardCharsets.UTF_8))); - - response.setResponseHeaders(responseHeaders); - - return response; - } catch (Exception e) { - e.printStackTrace(); - } - } - - if (null != loadingUrl.getPath() && - (loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START) || - loadingUrl.getPath().startsWith(Bridge.CAPACITOR_HTTPS_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 { - boolean isHttps = request.getUrl().getPath() != null - && request.getUrl().getPath().startsWith(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START); - - String urlString = request - .getUrl() - .toString() - .replace(bridge.getLocalUrl(), isHttps ? "https:/" : "http:/") - .replace(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START, "") - .replace(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START, ""); - urlString = URLDecoder.decode(urlString, "UTF-8"); - URL url = new URL(urlString); - JSObject headers = new JSObject(); - - for (Map.Entry 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 responseHeaders = new LinkedHashMap<>(); - for (Map.Entry> 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 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; - } - - 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")) { - 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; - } - - /** - * 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) { - final String method = request.getMethod(); - if (method.equals("GET")) { - try { - String url = request.getUrl().toString(); - Map headers = request.getRequestHeaders(); - boolean isHtmlText = false; - for (Map.Entry 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 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 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 uri. The handler - * will be invoked - * every time the shouldInterceptRequest method of the instance is - * called with - * a matching uri. - * - * @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 - * assetPath/... will be available under - * https://{uuid}.androidplatform.net/assets/.... - * - * @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 - * basePath/... will be available under - * https://{uuid}.androidplatform.net/.... - * - * @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; - } -} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.kt b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.kt new file mode 100644 index 00000000..96078c78 --- /dev/null +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/WebViewLocalServer.kt @@ -0,0 +1,852 @@ +/* +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 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.HttpRequestHandler +import com.getcapacitor.plugin.util.HttpRequestHandler.HttpURLConnectionBuilder +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import java.net.URLDecoder +import java.nio.charset.StandardCharsets +import java.util.Locale + +/** + * 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. + * + * + * This class is intended to be used from within the + * [android.webkit.WebViewClient.shouldInterceptRequest] + * and + * [android.webkit.WebViewClient.shouldInterceptRequest] + * methods. + */ +class WebViewLocalServer internal constructor( + context: Context, + private val bridge: Bridge, + private val jsInjector: JSInjector, + private val authorities: ArrayList, + // Whether to route all requests to paths without extensions back to + // `index.html` + private val html5mode: Boolean +) { + var basePath: String? = null + private set + + private val uriMatcher = UriMatcher(null) + private val protocolHandler = AndroidProtocolHandler(context.applicationContext) + private var isAsset = false + + /** + * A handler that produces responses for paths on the virtual asset server. + * + * + * Methods of this handler will be invoked on a background thread and care must + * be taken to + * correctly synchronize access to any shared state. + * + * + * 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. + */ + abstract class PathHandler @JvmOverloads constructor( + val encoding: String? = null, + val charset: String? = null, + val statusCode: Int = 200, + val reasonPhrase: String = "OK", + responseHeaders: MutableMap? = null + ) { + protected var mimeType: String? = null + val responseHeaders: MutableMap + + init { + val tempResponseHeaders = responseHeaders ?: HashMap() + tempResponseHeaders["Cache-Control"] = "no-cache" + this.responseHeaders = tempResponseHeaders + } + + fun handle(request: WebResourceRequest): InputStream? { + return handle(request.url) + } + + abstract fun handle(url: Uri): InputStream? + } + + /** + * Attempt to retrieve the WebResourceResponse associated with the given + * `request`. + * This method should be invoked from within + * [android.webkit.WebViewClient.shouldInterceptRequest]. + * + * @param request the request to process. + * @return a response if the request URL had a matching handler, null if no + * handler was found. + */ + fun shouldInterceptRequest(request: WebResourceRequest): WebResourceResponse? { + val loadingUrl = request.url + + if (loadingUrl.toString().endsWith("#image")) { + val headers: MutableMap = HashMap(request.requestHeaders) + headers.remove("x-requested-with") + + try { + val url = URL(loadingUrl.toString()) + val connection = url.openConnection() as HttpURLConnection + + for ((key, value) in headers) { + connection.setRequestProperty(key, value) + } + + var contentType = connection.contentType + if (contentType == null) { + contentType = "image/unknown" + } + + val inputStream = connection.inputStream + + return WebResourceResponse(contentType, "UTF-8", inputStream) + } catch (e: Exception) { + e.printStackTrace() + } + } + if (loadingUrl.toString().endsWith("#resolve")) { + val headers: MutableMap = HashMap(request.requestHeaders) + headers.remove("x-requested-with") + + try { + val url = URL(loadingUrl.toString()) + val connection = url.openConnection() as HttpURLConnection + + for ((key, value) in headers) { + connection.setRequestProperty(key, value) + } + + connection.instanceFollowRedirects = false + connection.connect() + + val locationHeader = connection.getHeaderField("Location") + val resolvedUrl = + if ((locationHeader != null)) locationHeader else loadingUrl.toString() + + val response = WebResourceResponse( + "text/plain", "UTF-8", + ByteArrayInputStream(resolvedUrl.toByteArray(StandardCharsets.UTF_8)) + ) + + val responseHeaders: MutableMap = HashMap() + for ((key, value) in connection.headerFields) { + if (key != null && value != null && !value.isEmpty()) { + responseHeaders[key] = value[0] + } + } + + responseHeaders["x-location"] = resolvedUrl + responseHeaders["Access-Control-Allow-Origin"] = "*" + responseHeaders["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" + response.responseHeaders = responseHeaders + + return response + } catch (e: Exception) { + e.printStackTrace() + } + } + + if (loadingUrl.toString().endsWith("#animevsub-vsub_extra")) { + val headers: MutableMap = HashMap(request.requestHeaders) + headers.remove("x-requested-with") + headers["referer"] = "https://animevietsub.tv" + + try { + val url = URL(loadingUrl.toString()) + val connection = url.openConnection() as HttpURLConnection + + for ((key, value) in headers) { + connection.setRequestProperty(key, value) + } + + connection.instanceFollowRedirects = false + connection.connect() + + val locationHeader = connection.getHeaderField("Location") + val resolvedUrl = + if ((locationHeader != null)) locationHeader else loadingUrl.toString() + + val responseHeaders: MutableMap = HashMap() + // for (Map.Entry> entry : + // connection.getHeaderFields().entrySet()) { + // if (entry.getKey() != null && entry.getValue() != null && + // !entry.getValue().isEmpty()) { + // responseHeaders.put(entry.getKey(), entry.getValue().get(0)); + // } + // } + // + responseHeaders["Access-Control-Allow-Origin"] = "*" + responseHeaders["Access-Control-Allow-Methods"] = + "PUT, GET, HEAD, POST, DELETE, OPTIONS" + + // responseHeaders.put("Location", resolvedUrl); + responseHeaders["W-Location"] = resolvedUrl + + val response = WebResourceResponse( + "text/plain", "UTF-8", + ByteArrayInputStream(resolvedUrl.toByteArray(StandardCharsets.UTF_8)) + ) + + response.responseHeaders = responseHeaders + + return response + } catch (e: Exception) { + e.printStackTrace() + } + } + + if (null != loadingUrl.path && + (loadingUrl.path!!.startsWith(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START) || + loadingUrl.path!!.startsWith(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START)) + ) { + Logger.Companion.debug("Handling CapacitorHttp request: $loadingUrl") + try { + return handleCapacitorHttpRequest(request) + } catch (e: Exception) { + Logger.Companion.error(e.localizedMessage) + return null + } + } + + var handler: PathHandler + synchronized(uriMatcher) { + handler = uriMatcher.match(request.url) as PathHandler + } + if (handler == null) { + return null + } + + if (isLocalFile(loadingUrl) || isMainUrl(loadingUrl) || !isAllowedUrl(loadingUrl) || isErrorUrl( + loadingUrl + ) + ) { + Logger.Companion.debug("Handling local request: " + request.url.toString()) + return handleLocalRequest(request, handler) + } else { + return handleProxyRequest(request, handler) + } + } + + private fun isLocalFile(uri: Uri): Boolean { + val path = uri.path + return path!!.startsWith(capacitorContentStart) || path.startsWith(capacitorFileStart) + } + + private fun isErrorUrl(uri: Uri): Boolean { + val url = uri.toString() + return url == bridge.errorUrl + } + + private fun isMainUrl(loadingUrl: Uri): Boolean { + return (bridge.serverUrl == null && loadingUrl.host.equals(bridge.host, ignoreCase = true)) + } + + private fun isAllowedUrl(loadingUrl: Uri): Boolean { + return !(bridge.serverUrl == null && !bridge.appAllowNavigationMask!!.matches(loadingUrl.host)) + } + + private fun getReasonPhraseFromResponseCode(code: Int): String { + return when (code) { + 100 -> "Continue" + 101 -> "Switching Protocols" + 200 -> "OK" + 201 -> "Created" + 202 -> "Accepted" + 203 -> "Non-Authoritative Information" + 204 -> "No Content" + 205 -> "Reset Content" + 206 -> "Partial Content" + 300 -> "Multiple Choices" + 301 -> "Moved Permanently" + 302 -> "Found" + 303 -> "See Other" + 304 -> "Not Modified" + 400 -> "Bad Request" + 401 -> "Unauthorized" + 403 -> "Forbidden" + 404 -> "Not Found" + 405 -> "Method Not Allowed" + 406 -> "Not Acceptable" + 407 -> "Proxy Authentication Required" + 408 -> "Request Timeout" + 409 -> "Conflict" + 410 -> "Gone" + 500 -> "Internal Server Error" + 501 -> "Not Implemented" + 502 -> "Bad Gateway" + 503 -> "Service Unavailable" + 504 -> "Gateway Timeout" + 505 -> "HTTP Version Not Supported" + else -> "Unknown" + } + } + + @Throws(IOException::class) + private fun handleCapacitorHttpRequest(request: WebResourceRequest): WebResourceResponse { + val isHttps = (request.url.path != null + && request.url.path!!.startsWith(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START)) + + var urlString: String? = request + .url + .toString() + .replace(bridge.localUrl!!, if (isHttps) "https:/" else "http:/") + .replace(Bridge.CAPACITOR_HTTP_INTERCEPTOR_START, "") + .replace(Bridge.CAPACITOR_HTTPS_INTERCEPTOR_START, "") + urlString = URLDecoder.decode(urlString, "UTF-8") + val url = URL(urlString) + val headers = JSObject() + + for ((key, value) in request.requestHeaders) { + headers.put(key, value) + } + + val connectionBuilder = HttpURLConnectionBuilder() + .setUrl(url) + .setMethod(request.method) + .setHeaders(headers) + .openConnection() + + val connection = connectionBuilder.build() + + if (!HttpRequestHandler.isDomainExcludedFromSSL(bridge, url)) { + connection.setSSLSocketFactory(bridge) + } + + connection.connect() + + var mimeType: String? = null + var encoding: String? = null + val responseHeaders: MutableMap = LinkedHashMap() + for ((key, value1) in connection.headerFields) { + val builder = StringBuilder() + for (value in value1) { + builder.append(value) + builder.append(", ") + } + builder.setLength(builder.length - 2) + + if ("Content-Type".equals(key, ignoreCase = true)) { + val contentTypeParts = + builder.toString().split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + mimeType = contentTypeParts[0].trim { it <= ' ' } + if (contentTypeParts.size > 1) { + val encodingParts = + contentTypeParts[1].split("=".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + if (encodingParts.size > 1) { + encoding = encodingParts[1].trim { it <= ' ' } + } + } + } else { + responseHeaders[key] = builder.toString() + } + } + + var inputStream = connection.errorStream + if (inputStream == null) { + inputStream = connection.inputStream + } + + if (null == mimeType) { + mimeType = getMimeType(request.url.path, inputStream) + } + + val responseCode = connection.responseCode + val reasonPhrase = getReasonPhraseFromResponseCode(responseCode) + + return WebResourceResponse( + mimeType, + encoding, + responseCode, + reasonPhrase, + responseHeaders, + inputStream + ) + } + + private fun handleLocalRequest( + request: WebResourceRequest, + handler: PathHandler + ): WebResourceResponse? { + val path = request.url.path + + if (request.requestHeaders["Range"] != null) { + val responseStream: InputStream = LollipopLazyInputStream(handler, request) + val mimeType = getMimeType(path, responseStream) + val tempResponseHeaders = handler.responseHeaders + var statusCode = 206 + try { + val totalRange = responseStream.available() + val rangeString = request.requestHeaders["Range"] + val parts = + rangeString!!.split("=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val streamParts = + parts[1].split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray() + val fromRange = streamParts[0] + var range = totalRange - 1 + if (streamParts.size > 1) { + range = streamParts[1].toInt() + } + tempResponseHeaders["Accept-Ranges"] = "bytes" + tempResponseHeaders["Content-Range"] = "bytes $fromRange-$range/$totalRange" + } catch (e: IOException) { + statusCode = 404 + } + return WebResourceResponse( + mimeType, + handler.encoding, + statusCode, + handler.reasonPhrase, + tempResponseHeaders, + responseStream + ) + } + + if (isLocalFile(request.url) || isErrorUrl(request.url)) { + val responseStream: InputStream = LollipopLazyInputStream(handler, request) + val mimeType = getMimeType(request.url.path, responseStream) + val statusCode = getStatusCode(responseStream, handler.statusCode) + return WebResourceResponse( + mimeType, + handler.encoding, + statusCode, + handler.reasonPhrase, + handler.responseHeaders, + responseStream + ) + } + + if (path == "/cordova.js") { + return WebResourceResponse( + "application/javascript", + handler.encoding, + handler.statusCode, + handler.reasonPhrase, + handler.responseHeaders, + null + ) + } + + if (path == "/" || (!request.url.lastPathSegment!!.contains(".") && html5mode)) { + var responseStream: InputStream? + try { + var startPath = this.basePath + "/index.html" + if (bridge.routeProcessor != null) { + val processedRoute = + bridge.routeProcessor!!.process(this.basePath, "/index.html") + startPath = processedRoute.path + isAsset = processedRoute!!.isAsset + } + + responseStream = if (isAsset) { + protocolHandler.openAsset(startPath) + } else { + protocolHandler.openFile(startPath) + } + } catch (e: IOException) { + Logger.Companion.error("Unable to open index.html", e) + return null + } + + responseStream = jsInjector.getInjectedStream(responseStream) + + val statusCode = getStatusCode(responseStream, handler.statusCode) + return WebResourceResponse( + "text/html", + handler.encoding, + statusCode, + handler.reasonPhrase, + handler.responseHeaders, + responseStream + ) + } + + if ("/favicon.ico".equals(path, ignoreCase = true)) { + try { + return WebResourceResponse("image/png", null, null) + } catch (e: Exception) { + Logger.Companion.error("favicon handling failed", e) + } + } + + val periodIndex = path!!.lastIndexOf(".") + if (periodIndex >= 0) { + val ext = path.substring(path.lastIndexOf(".")) + + var responseStream: InputStream? = LollipopLazyInputStream(handler, request) + + // TODO: Conjure up a bit more subtlety than this + if (ext == ".html") { + responseStream = jsInjector.getInjectedStream(responseStream) + } + + val mimeType = getMimeType(path, responseStream) + val statusCode = getStatusCode(responseStream, handler.statusCode) + return WebResourceResponse( + mimeType, + handler.encoding, + statusCode, + handler.reasonPhrase, + handler.responseHeaders, + responseStream + ) + } + + return null + } + + /** + * 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 fun handleProxyRequest( + request: WebResourceRequest, + handler: PathHandler + ): WebResourceResponse? { + val method = request.method + if (method == "GET") { + try { + val url = request.url.toString() + val headers = request.requestHeaders + var isHtmlText = false + for ((key, value) in headers) { + if (key.equals("Accept", ignoreCase = true) + && value.lowercase(Locale.getDefault()).contains("text/html") + ) { + isHtmlText = true + break + } + } + if (isHtmlText) { + val conn = URL(url).openConnection() as HttpURLConnection + for ((key, value) in headers) { + conn.setRequestProperty(key, value) + } + val getCookie = CookieManager.getInstance().getCookie(url) + if (getCookie != null) { + conn.setRequestProperty("Cookie", getCookie) + } + conn.requestMethod = method + conn.readTimeout = 30 * 1000 + conn.connectTimeout = 30 * 1000 + if (request.url.userInfo != null) { + val userInfoBytes = + request.url.userInfo!!.toByteArray(StandardCharsets.UTF_8) + val base64 = Base64.encodeToString(userInfoBytes, Base64.NO_WRAP) + conn.setRequestProperty("Authorization", "Basic $base64") + } + + val cookies = conn.headerFields["Set-Cookie"] + if (cookies != null) { + for (cookie in cookies) { + CookieManager.getInstance().setCookie(url, cookie) + } + } + var responseStream = conn.inputStream + responseStream = jsInjector.getInjectedStream(responseStream) + return WebResourceResponse( + "text/html", + handler.encoding, + handler.statusCode, + handler.reasonPhrase, + handler.responseHeaders, + responseStream + ) + } + } catch (ex: Exception) { + bridge.handleAppUrlLoadError(ex) + } + } + return null + } + + private fun getMimeType(path: String?, stream: InputStream?): String? { + var mimeType: String? = null + try { + mimeType = URLConnection.guessContentTypeFromName(path) // Does not recognize *.js + if (mimeType != null && path!!.endsWith(".js") && mimeType == "image/x-icon") { + Logger.Companion.debug("We shouldn't be here") + } + if (mimeType == null) { + mimeType = if (path!!.endsWith(".js") || path.endsWith(".mjs")) { + // Make sure JS files get the proper mimetype to support ES modules + "application/javascript" + } else if (path.endsWith(".wasm")) { + "application/wasm" + } else { + URLConnection.guessContentTypeFromStream(stream) + } + } + } catch (ex: Exception) { + Logger.Companion.error("Unable to get mime type$path", ex) + } + return mimeType + } + + private fun getStatusCode(stream: InputStream?, defaultCode: Int): Int { + var finalStatusCode = defaultCode + try { + if (stream!!.available() == -1) { + finalStatusCode = 404 + } + } catch (e: IOException) { + finalStatusCode = 500 + } + return finalStatusCode + } + + /** + * Registers a handler for the given `uri`. The `handler` + * will be invoked + * every time the `shouldInterceptRequest` method of the instance is + * called with + * a matching `uri`. + * + * @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. + */ + fun register(uri: Uri, handler: PathHandler?) { + synchronized(uriMatcher) { + uriMatcher.addURI(uri.scheme, uri.authority, uri.path, handler) + } + } + + /** + * Hosts the application's assets on an https:// URL. Assets from the local path + * `assetPath/...` will be available under + * `https://{uuid}.androidplatform.net/assets/...`. + * + * @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. + */ + fun hostAssets(assetPath: String?) { + this.isAsset = true + this.basePath = assetPath + createHostingDetails() + } + + /** + * Hosts the application's files on an https:// URL. Files from the basePath + * `basePath/...` will be available under + * `https://{uuid}.androidplatform.net/...`. + * + * @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. + */ + fun hostFiles(basePath: String?) { + this.isAsset = false + this.basePath = basePath + createHostingDetails() + } + + private fun createHostingDetails() { + val assetPath = this.basePath + + require(assetPath!!.indexOf('*') == -1) { "assetPath cannot contain the '*' character." } + + val handler: PathHandler = object : PathHandler() { + override fun handle(url: Uri): InputStream? { + var stream: InputStream? = null + var path = url.path + + // Pass path to routeProcessor if present + val routeProcessor = bridge.routeProcessor + var ignoreAssetPath = false + if (routeProcessor != null) { + val processedRoute = bridge.routeProcessor!!.process("", path) + path = processedRoute.path + 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.path + } + + stream = protocolHandler.openFile(path) + } else if (ignoreAssetPath) { + stream = protocolHandler.openAsset(path) + } else { + stream = protocolHandler.openAsset(assetPath + path) + } + } catch (e: IOException) { + Logger.Companion.error("Unable to open asset URL: $url") + return null + } + + return stream + } + } + + for (authority in authorities) { + registerUriForScheme(Bridge.CAPACITOR_HTTP_SCHEME, handler, authority) + registerUriForScheme(Bridge.CAPACITOR_HTTPS_SCHEME, handler, authority) + + val customScheme = bridge.scheme + if (customScheme != Bridge.CAPACITOR_HTTP_SCHEME + && customScheme != Bridge.CAPACITOR_HTTPS_SCHEME + ) { + registerUriForScheme(customScheme, handler, authority) + } + } + } + + private fun registerUriForScheme(scheme: String, handler: PathHandler, authority: String) { + val uriBuilder = Uri.Builder() + uriBuilder.scheme(scheme) + uriBuilder.authority(authority) + uriBuilder.path("") + val 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 class LazyInputStream(protected val handler: PathHandler) : InputStream() { + private var `is`: InputStream? = null + + private val inputStream: InputStream? + get() { + if (`is` == null) { + `is` = handle() + } + return `is` + } + + protected abstract fun handle(): InputStream? + + @Throws(IOException::class) + override fun available(): Int { + val `is` = inputStream + return if ((`is` != null)) `is`.available() else -1 + } + + @Throws(IOException::class) + override fun read(): Int { + val `is` = inputStream + return if ((`is` != null)) `is`.read() else -1 + } + + @Throws(IOException::class) + override fun read(b: ByteArray): Int { + val `is` = inputStream + return if ((`is` != null)) `is`.read(b) else -1 + } + + @Throws(IOException::class) + override fun read(b: ByteArray, off: Int, len: Int): Int { + val `is` = inputStream + return if ((`is` != null)) `is`.read(b, off, len) else -1 + } + + @Throws(IOException::class) + override fun skip(n: Long): Long { + val `is` = inputStream + return if ((`is` != null)) `is`.skip(n) else 0 + } + } + + // For L and above. + private class LollipopLazyInputStream( + handler: PathHandler, + private val request: WebResourceRequest + ) : LazyInputStream(handler) { + private val `is`: InputStream? = null + + override fun handle(): InputStream? { + return handler.handle(request) + } + } + + companion object { + private const val capacitorFileStart = Bridge.CAPACITOR_FILE_START + private const val capacitorContentStart = Bridge.CAPACITOR_CONTENT_START + private fun parseAndVerifyUrl(url: String?): Uri? { + if (url == null) { + return null + } + val uri = Uri.parse(url) + if (uri == null) { + Logger.Companion.error("Malformed URL: $url") + return null + } + val path = uri.path + if (path == null || path.isEmpty()) { + Logger.Companion.error("URL does not have a path: $url") + return null + } + return uri + } + } +} diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java index 45c01be2..ac07202a 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorCookies.java @@ -35,12 +35,12 @@ protected void handleOnDestroy() { @JavascriptInterface public boolean isEnabled() { - PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies"); + PluginConfig pluginConfig = bridge.getConfig().getPluginConfiguration("CapacitorCookies"); return pluginConfig.getBoolean("enabled", false); } private boolean isAllowingInsecureCookies() { - PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorCookies"); + PluginConfig pluginConfig = bridge.getConfig().getPluginConfiguration("CapacitorCookies"); return pluginConfig.getBoolean("androidCustomSchemeAllowInsecureAccess", false); } diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java index 46bc1741..12cff717 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/CapacitorHttp.java @@ -41,17 +41,16 @@ protected void handleOnDestroy() { Runnable job = entry.getKey(); PluginCall call = entry.getValue(); - if (call.getData().has("activeCapacitorHttpUrlConnection")) { + if (call.data.has("activeCapacitorHttpUrlConnection")) { try { - CapacitorHttpUrlConnection connection = (CapacitorHttpUrlConnection) call - .getData() + CapacitorHttpUrlConnection connection = (CapacitorHttpUrlConnection) call.data .get("activeCapacitorHttpUrlConnection"); connection.disconnect(); - call.getData().remove("activeCapacitorHttpUrlConnection"); + call.data.remove("activeCapacitorHttpUrlConnection"); } catch (Exception ignored) {} } - getBridge().releaseCall(call); + bridge.releaseCall(call); } activeRequests.clear(); @@ -63,7 +62,7 @@ private void http(final PluginCall call, final String httpMethod) { @Override public void run() { try { - JSObject response = HttpRequestHandler.request(call, httpMethod, getBridge()); + JSObject response = HttpRequestHandler.request(call, httpMethod, bridge); call.resolve(response); } catch (Exception e) { call.reject(e.getLocalizedMessage(), e.getClass().getSimpleName(), e); @@ -83,7 +82,7 @@ public void run() { @JavascriptInterface public boolean isEnabled() { - PluginConfig pluginConfig = getBridge().getConfig().getPluginConfiguration("CapacitorHttp"); + PluginConfig pluginConfig = bridge.getConfig().getPluginConfiguration("CapacitorHttp"); return pluginConfig.getBoolean("enabled", false); } diff --git a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java index 6e4bb747..c446b93a 100644 --- a/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java +++ b/@capacitor/android/capacitor/src/main/java/com/getcapacitor/plugin/util/HttpRequestHandler.java @@ -418,19 +418,19 @@ public static JSObject request(PluginCall call, String httpMethod, Bridge bridge // Set HTTP body on a non GET or HEAD request if (isHttpMutate) { JSValue data = new JSValue(call, "data"); - if (data.getValue() != null) { + if (data.value != null) { connection.setDoOutput(true); connection.setRequestBody(call, data, dataType); } } - call.getData().put("activeCapacitorHttpUrlConnection", connection); + call.data.put("activeCapacitorHttpUrlConnection", connection); connection.connect(); JSObject response = buildResponse(connection, responseType); connection.disconnect(); - call.getData().remove("activeCapacitorHttpUrlConnection"); + call.data.remove("activeCapacitorHttpUrlConnection"); return response; } diff --git a/bun.lockb b/bun.lockb index fda8284c..6679e602 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src-capacitor/android/app/build.gradle b/src-capacitor/android/app/build.gradle index e9b213b5..488eea7d 100644 --- a/src-capacitor/android/app/build.gradle +++ b/src-capacitor/android/app/build.gradle @@ -1,6 +1,12 @@ apply plugin: 'com.android.application' android { + + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + jvmTarget = '17' + } + } namespace "git.shin.animevsub" compileSdkVersion rootProject.ext.compileSdkVersion defaultConfig { @@ -38,6 +44,7 @@ dependencies { } apply from: 'capacitor.build.gradle' +apply plugin: 'org.jetbrains.kotlin.android' try { def servicesJSON = file('google-services.json') diff --git a/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.java b/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.java deleted file mode 100644 index bbbd83f9..00000000 --- a/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.java +++ /dev/null @@ -1,24 +0,0 @@ -package git.shin.animevsub; - -import android.os.Bundle; - -import com.getcapacitor.BridgeActivity; -import com.getcapacitor.Plugin; - -import java.util.ArrayList; - -public class MainActivity extends BridgeActivity { - // no code -// @Override -// public void onCreate() { -// registerPlugin(ResolvePlugin.class); -// super.onStart(); -// bridge.getWebView().setVerticalScrollBarEnabled(false); -// } - @Override - public void onCreate(Bundle savedInstanceState) { - registerPlugin(ResolvePlugin.class); - super.onCreate(savedInstanceState); - bridge.getWebView().setVerticalScrollBarEnabled(false); - } -} diff --git a/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.kt b/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.kt new file mode 100644 index 00000000..0b6d5481 --- /dev/null +++ b/src-capacitor/android/app/src/main/java/git/shin/animevsub/MainActivity.kt @@ -0,0 +1,19 @@ +package git.shin.animevsub + +import android.os.Bundle +import com.getcapacitor.BridgeActivity + +class MainActivity : BridgeActivity() { + // no code + // @Override + // public void onCreate() { + // registerPlugin(ResolvePlugin.class); + // super.onStart(); + // bridge.getWebView().setVerticalScrollBarEnabled(false); + // } + public override fun onCreate(savedInstanceState: Bundle?) { + registerPlugin(ResolvePlugin::class.java) + super.onCreate(savedInstanceState) + bridge.webView.isVerticalScrollBarEnabled = false + } +} diff --git a/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.java b/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.java deleted file mode 100644 index 0695b441..00000000 --- a/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.java +++ /dev/null @@ -1,112 +0,0 @@ -package git.shin.animevsub; - -import com.getcapacitor.JSArray; -import com.getcapacitor.JSObject; -import com.getcapacitor.Plugin; -import com.getcapacitor.PluginCall; -import com.getcapacitor.PluginMethod; -import com.getcapacitor.annotation.CapacitorPlugin; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.IOException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.Iterator; -import java.util.List; -import java.util.ArrayList; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.stream.Collectors; - -@CapacitorPlugin(name = "Resolve") -public class ResolvePlugin extends Plugin { - - @PluginMethod() - public void echo(PluginCall call) { - String value = call.getString("value"); - - JSObject ret = new JSObject(); - ret.put("value", value); - call.resolve(ret); - } - - @PluginMethod() - public void resolve(PluginCall call) { - String url = call.getString("url"); - JSObject headers = call.getObject("headers"); - - if (url == null) { - call.reject("Must provide a URL"); - return; - } - - JSObject ret = resolveUrl(url, headers); - if (ret.has("error")) { - call.reject(ret.getString("error")); - } else { - call.resolve(ret); - } - } - @PluginMethod() - public void resolveAll(PluginCall call) { - JSArray urlArray = call.getArray("urls"); -// JSArray headersArray = call.getArray("headers") ?? []; - - if (urlArray == null) { - call.reject("Must provide an array of URLs"); - return; - } - - try { - List> futures = new ArrayList<>(); - List urls = urlArray.toList(); - - List results = new ArrayList<>(); - - for (JSONObject jsonObject : urls) { - JSObject urlObject = JSObject.fromJSONObject(jsonObject); - - String url = urlObject.getString("url"); - JSObject headers = urlObject.getJSObject("headers"); - - results.add(resolveUrl(url, headers)); - } - - call.resolve(new JSObject().put("results", new JSArray(results))); - } catch (JSONException e) { - call.reject("Error processing URLs array: " + e.getMessage()); - } - } - - private JSObject resolveUrl(String url, JSObject headers) { - JSObject result = new JSObject(); - - try { - URL obj = new URL(url); - HttpURLConnection connection = (HttpURLConnection) obj.openConnection(); - - // Set headers - if (headers != null) { - Iterator keys = headers.keys(); - while (keys.hasNext()) { - String key = keys.next(); - connection.setRequestProperty(key, headers.getString(key)); - } - } - - connection.setInstanceFollowRedirects(false); - connection.connect(); - - String locationHeader = connection.getHeaderField("Location"); - String resolvedUrl = (locationHeader != null) ? locationHeader : url; - - result.put("url", resolvedUrl); - } catch (IOException e) { - result.put("error", e.getMessage()); - } - - return result; - } -} diff --git a/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.kt b/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.kt new file mode 100644 index 00000000..fb456229 --- /dev/null +++ b/src-capacitor/android/app/src/main/java/git/shin/animevsub/ResolvePlugin.kt @@ -0,0 +1,105 @@ +package git.shin.animevsub + +import com.getcapacitor.JSArray +import com.getcapacitor.JSObject +import com.getcapacitor.Plugin +import com.getcapacitor.PluginCall +import com.getcapacitor.PluginMethod +import com.getcapacitor.annotation.CapacitorPlugin +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.net.HttpURLConnection +import java.net.URL +import java.util.concurrent.CompletableFuture + +@CapacitorPlugin(name = "Resolve") +class ResolvePlugin : Plugin() { + @PluginMethod + fun echo(call: PluginCall) { + val value = call.getString("value") + + val ret = JSObject() + ret.put("value", value) + call.resolve(ret) + } + + @PluginMethod + fun resolve(call: PluginCall) { + val url = call.getString("url") + val headers = call.getObject("headers") + + if (url == null) { + call.reject("Must provide a URL") + return + } + + val ret = resolveUrl(url, headers) + if (ret.has("error")) { + call.reject(ret.getString("error")) + } else { + call.resolve(ret) + } + } + + @PluginMethod + fun resolveAll(call: PluginCall) { + val urlArray = call.getArray("urls") + + // JSArray headersArray = call.getArray("headers") ?? []; + if (urlArray == null) { + call.reject("Must provide an array of URLs") + return + } + + try { + val futures: List> = ArrayList() + val urls = urlArray.toList() + + val results: MutableList = ArrayList() + + for (jsonObject in urls) { + val urlObject = JSObject.fromJSONObject(jsonObject) + + val url = urlObject.getString("url") + val headers = urlObject.getJSObject("headers") + + results.add(resolveUrl(url, headers)) + } + + call.resolve(JSObject().put("results", JSArray(results))) + } catch (e: JSONException) { + call.reject("Error processing URLs array: " + e.message) + } + } + + private fun resolveUrl(url: String?, headers: JSObject?): JSObject { + val result = JSObject() + + try { + val obj = URL(url) + val connection = obj.openConnection() as HttpURLConnection + + // Set headers + if (headers != null) { + val keys = headers.keys() + while (keys.hasNext()) { + val key = keys.next() + connection.setRequestProperty(key, headers.getString(key)) + } + } + + connection.instanceFollowRedirects = false + connection.connect() + + val locationHeader = connection.getHeaderField("Location") + val resolvedUrl = if ((locationHeader != null)) locationHeader else url!! + + result.put("url", resolvedUrl) + } catch (e: IOException) { + result.put("error", e.message) + } + + return result + } +} diff --git a/src-capacitor/android/build.gradle b/src-capacitor/android/build.gradle index 37d63ae4..0e437570 100644 --- a/src-capacitor/android/build.gradle +++ b/src-capacitor/android/build.gradle @@ -1,7 +1,10 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - + + ext { + kotlin_version = '2.0.0' + } repositories { google() mavenCentral() @@ -9,6 +12,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.0.0' classpath 'com.google.gms:google-services:4.3.15' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/src-capacitor/android/capacitor.settings.gradle b/src-capacitor/android/capacitor.settings.gradle index 0203f144..2b270dad 100644 --- a/src-capacitor/android/capacitor.settings.gradle +++ b/src-capacitor/android/capacitor.settings.gradle @@ -1,36 +1,36 @@ // DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +project(':capacitor-android').projectDir = new File('../../@capacitor/android/capacitor') include ':capacitor-community-firebase-analytics' -project(':capacitor-community-firebase-analytics').projectDir = new File('../node_modules/.pnpm/@capacitor-community+firebase-analytics@5.0.1_@capacitor+core@5.7.6/node_modules/@capacitor-community/firebase-analytics/android') +project(':capacitor-community-firebase-analytics').projectDir = new File('../node_modules/@capacitor-community/firebase-analytics/android') include ':capacitor-app' -project(':capacitor-app').projectDir = new File('../node_modules/.pnpm/@capacitor+app@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/app/android') +project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android') include ':capacitor-browser' -project(':capacitor-browser').projectDir = new File('../node_modules/.pnpm/@capacitor+browser@5.2.1_@capacitor+core@5.7.6/node_modules/@capacitor/browser/android') +project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android') include ':capacitor-device' -project(':capacitor-device').projectDir = new File('../node_modules/.pnpm/@capacitor+device@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/device/android') +project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/device/android') include ':capacitor-filesystem' -project(':capacitor-filesystem').projectDir = new File('../node_modules/.pnpm/@capacitor+filesystem@5.2.2_@capacitor+core@5.7.6/node_modules/@capacitor/filesystem/android') +project(':capacitor-filesystem').projectDir = new File('../node_modules/@capacitor/filesystem/android') include ':capacitor-haptics' -project(':capacitor-haptics').projectDir = new File('../node_modules/.pnpm/@capacitor+haptics@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/haptics/android') +project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android') include ':capacitor-preferences' -project(':capacitor-preferences').projectDir = new File('../node_modules/.pnpm/@capacitor+preferences@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/preferences/android') +project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') include ':capacitor-share' -project(':capacitor-share').projectDir = new File('../node_modules/.pnpm/@capacitor+share@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/share/android') +project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') include ':capacitor-status-bar' -project(':capacitor-status-bar').projectDir = new File('../node_modules/.pnpm/@capacitor+status-bar@5.0.8_@capacitor+core@5.7.6/node_modules/@capacitor/status-bar/android') +project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') include ':hugotomazi-capacitor-navigation-bar' -project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/.pnpm/@hugotomazi+capacitor-navigation-bar@3.0.0_@capacitor+core@5.7.6/node_modules/@hugotomazi/capacitor-navigation-bar/android') +project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android') include ':jcesarmobile-ssl-skip' -project(':jcesarmobile-ssl-skip').projectDir = new File('../node_modules/.pnpm/@jcesarmobile+ssl-skip@0.4.0/node_modules/@jcesarmobile/ssl-skip/android') +project(':jcesarmobile-ssl-skip').projectDir = new File('../node_modules/@jcesarmobile/ssl-skip/android') diff --git a/src-capacitor/bun.lockb b/src-capacitor/bun.lockb index 3cfc0a14..4b4a8622 100755 Binary files a/src-capacitor/bun.lockb and b/src-capacitor/bun.lockb differ