Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Capacitor on Android detected as web platform after redirecting with allowNavigation #7454

Open
1 of 3 tasks
Dylancyclone opened this issue May 7, 2024 · 8 comments · May be fixed by #7571
Open
1 of 3 tasks
Labels
bug type: bug A confirmed bug report

Comments

@Dylancyclone
Copy link

Capacitor Version

💊 Capacitor Doctor 💊

Latest Dependencies:

@capacitor/cli: 6.0.0
@capacitor/core: 6.0.0
@capacitor/android: 6.0.0
@capacitor/ios: 6.0.0

Installed Dependencies:

@capacitor/cli: 6.0.0
@capacitor/core: 6.0.0
@capacitor/ios: 6.0.0
@capacitor/android: 6.0.0

[success] iOS looking great! 👌
[success] Android looking great! 👌

Other API Details

`npm --version`
> 10.7.0

`node --version`
> v20.11.0

Platforms Affected

  • iOS
  • Android
  • Web

Current Behavior

After redirecting to an external URL that is allowed in the server.allowNavigation config setting, an android app reports being on a web platform instead of android.
This can be seen by calling Capacitor.getPlatform() (which returns android before redirecting and web after redirecting) or by trying to use a native API, which will either fallback to the web version of the API, or fail to run at all

All this works on iOS, where after redirecting, the app can still use native iOS APIs

Expected Behavior

After redirecting, the android app should still have access to all the native Android APIs (and report itself as being an Android platform)

Project Reproduction

https://github.com/Dylancyclone/capacitor-android-redirect/tree/main

Additional Information

Reproduction details are in the repo, but the basic reproduction steps are as follows:

  1. Serve a valid capacitor app on your computer (i.e. with npx serve .)
  2. Add your computer's IP to the server.allowNavigation capacitor config setting
  3. Load the application on your (real or simulated) device
  4. Notice that Capacitor.getPlatform() returns android on android and ios on iOS
  5. Redirect to the hosted server (any method works, can be window.location.assign(), window.location.href = "", window.location.replace(), or redirecting through a form
  6. Notice that Capacitor.getPlatform() now returns web on android but still ios on iOS

Possibly related to #5455

Here are videos showing the issue. Even though these were recorded on a simulator, the results are identical to a physical device

android.mov
ios.mov

Our use case is to have one app that connects to different backends on different subdomains, very similar to Slack. So far everything is working except the android app loses access to its native APIs

@Dylancyclone
Copy link
Author

Dylancyclone commented May 8, 2024

So it looks like there are two parts to this issue: redirecting to a local address, and capacitor not injecting the JS bridge into external pages. Again, both of these are only relevant to Android, the iOS side works perfectly

For the first issue, if we try to navigate to a computer on the local network (http, through ip address or [computername].local) we would get the issue described above where the platform would be seen as web and the plugins would use the web version or fail entirely. If we instead redirect to a remotely hosted server (https), we get a slightly different issue.

If we add our remote server to the server.allowNavigation config and redirect to it using the steps above, the platform is reported as android correctly, but all plugins are completely broken:

image

Investigating this issue, we found the root cause to be the JavaScript injection in Bridge.java. We notice that if WebViewFeature.DOCUMENT_START_SCRIPTis supported, the javascript is only injected into the base URL (if it is NOT supported (i.e by commenting the if guard out), the injector always works). If the addDocumentStartJavaScript() function were changed to inject on all allowedOriginRules (the base URL and everything in the server.allowNavigation setting) instead of just the base URL, all plugins work again.

Here's the diff:

Original:

// Start the local web server
JSInjector injector = getJSInjector();
if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
String allowedOrigin = appUrl;
Uri appUri = Uri.parse(appUrl);
if (appUri.getPath() != null) {
if (appUri.getPath().equals("/")) {
allowedOrigin = appUrl.substring(0, appUrl.length() - 1);
} else {
allowedOrigin = appUri.toString().replace(appUri.getPath(), "");
}
}
WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), Collections.singleton(allowedOrigin));
injector = null;
}
localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);

New:

        // Start the local web server
        JSInjector injector = getJSInjector();
        if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
            WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), allowedOriginRules);
            injector = null;
        }
        localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);

I am new to capacitor and have no idea if this would have any side effects, but this seems to fix our issue completely and bring parity with the iOS version.


So to summarize:

The original issue still exists while redirecting to a computer on a local network (or just over http, I don't have the capability to test that right now)

There also appears to be a bug when redirecting to a remote server (again, maybe just over https) caused by the native bridge not injecting capacitor's Javascript on remote pages. A fix we found was to make the change above, but I'd rather ask if it is the correct approach before creating a pull request.

@danbritt
Copy link

I'm experiencing this as well. I use the allowNavigation to switch between different stages of the app, and this prevents that workflow. Thank you for all of the digging you did, the detailed explanation, and workaround!

@Vadinci
Copy link

Vadinci commented Jun 3, 2024

We are experiencing this issue as well, and I cannot thank you enough for having found this workaround 🙇 I can confirm that changing the addDocumentStartJavaScript call to just use allowedOriginRules fixes the issues for https requests on local networks. Using the fallback jsInjector does not however.

I have hacked together a quick "patch" that we call in the MainActivity, to avoid editing the Bridge.Java file directly (as it gets messy with version control). Here it is in case anyone wants to use it (call it after onCreate and before loading any other pages):

class MainActivity : BridgeActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        /**
         * ...
         */
        patchJSInjection();
    }

    /**
     * ...
     */

    private fun patchJSInjection() {
        try {
            if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
                val getJsInjector = bridge::class.java.declaredMethods.single { it.name == "getJSInjector" };
                getJsInjector.isAccessible = true;
                val injector = getJsInjector.invoke(bridge);

                val getScriptString = injector::class.java.declaredMethods.single { it.name === "getScriptString" };
                val scriptString = getScriptString.invoke(injector) as String;

                val allowedOrigins: MutableSet<String> = mutableSetOf();
                // Add origins that the Capacitor JS Bridge should be injected into
                allowedOrigins.add("https://www.foo.bar");

                WebViewCompat.addDocumentStartJavaScript(bridge.webView, scriptString, allowedOrigins)
            }
        }catch (e: Exception) {
            Log.e("Error", e.message ?: "");
        }
    }
}

@jarredhawkins
Copy link

I have hacked together a quick "patch" that we call in the MainActivity, to avoid editing the Bridge.Java file directly (as it gets messy with version control). Here it is in case anyone wants to use it (call it after onCreate and before loading any other pages):

@Vadinci huge thanks for that snippet. I hacked together similar in java:

private void patchJSInjection() {
        try {
            if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
                var getJsInjector = Arrays.stream(bridge.getClass().getDeclaredMethods())
                        .filter(method -> method.getName().equals("getJSInjector"))
                        .findFirst()
                        .get();

                getJsInjector.setAccessible(true);
                var injector = getJsInjector.invoke(bridge);

                var getScriptString = Arrays.stream(injector.getClass().getDeclaredMethods())
                        .filter(method -> method.getName().equals("getScriptString"))
                        .findFirst()
                        .get();
                var scriptString = (String) getScriptString.invoke(injector);

                var allowedOrigins = Arrays.stream(bridge.getConfig().getAllowNavigation())
                        .filter(str -> str.contains("yourdomain.com") || str.contains("otherdomain.com"))
                        .filter(str -> str.contains("https://"))
                        // WebViewCompat likes things formatted particularly, trim trailing /*
                        .map(str -> str.replaceAll("/\\*$", ""))
                        .collect(Collectors.toSet());

                Logger.info("patchJSInjection", "Injecting custom rules " + allowedOrigins);
                WebViewCompat.addDocumentStartJavaScript(bridge.getWebView(), scriptString, allowedOrigins);
            }
        } catch (Exception e) {
            Logger.error( e.getMessage(), e);
        }
}

@Cow258
Copy link

Cow258 commented Jun 20, 2024

I got same issue too.

image
image

To resolve this issue, I apply a patch to Bridge.java after it has been built

const xfs = require('fs/promises')

const { glob } = require('glob')

module.exports = async () => {

  const originPatten = `\
        // Start the local web server
        JSInjector injector = getJSInjector();
        if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
            String allowedOrigin = appUrl;
            Uri appUri = Uri.parse(appUrl);
            if (appUri.getPath() != null) {
                if (appUri.getPath().equals("/")) {
                    allowedOrigin = appUrl.substring(0, appUrl.length() - 1);
                } else {
                    allowedOrigin = appUri.toString().replace(appUri.getPath(), "");
                }
            }
            WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), Collections.singleton(allowedOrigin));
            injector = null;
        }
        localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);`.split('\n')
  const replacePatten = `\
        // Patched local web server
        JSInjector injector = getJSInjector();
        if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
            WebViewCompat.addDocumentStartJavaScript(webView, injector.getScriptString(), allowedOriginRules);
            injector = null;
        }
        localServer = new WebViewLocalServer(context, this, injector, authorities, html5mode);`.split('\n')

  let modified = false
  for (const path of await glob('node_modules/.pnpm/**/Bridge.java')) {
    const source = await xfs.readFile(path, 'utf8')
    const lines = source.split('\n')

    for (let i = 0; i < lines.length; i++) {
      const line = lines[i]

      if (line.includes('// Start the local web server')) {
        if (lines.slice(i, i + originPatten.length).join('\n') !== originPatten.join('\n')) {
          console.log('Skipped: Pattern not matched')
          continue
        }

        console.log('Modified: Pattern matched')
        lines.splice(i, originPatten.length, ...replacePatten)
        modified = true
        break
      }
    }

    if (modified) {
      await xfs.writeFile(path, lines.join('\n'), 'utf8')
      break
    }
  }
}

@rfe-css
Copy link

rfe-css commented Jul 15, 2024

In our case the issue was, that the script did not get injected on allowNavigation pages because it is not provided to the addDocumentStartJavaScript function:

image

i also recommend using https://www.npmjs.com/package/patch-package for all these patches until it is being resolved.

Copy link

ionitron-bot bot commented Oct 10, 2024

This issue has been labeled as type: bug. This label is added to issues that that have been reproduced and are being tracked in our internal issue tracker.

@schniper
Copy link

schniper commented Oct 19, 2024

I have hacked together a quick "patch" that we call in the MainActivity, to avoid editing the Bridge.Java file directly (as it gets messy with version control). Here it is in case anyone wants to use it (call it after onCreate and before loading any other pages):

@Vadinci huge thanks for that snippet. I hacked together similar in java:

private void patchJSInjection() {
        try {
            if (WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)) {
                var getJsInjector = Arrays.stream(bridge.getClass().getDeclaredMethods())
                        .filter(method -> method.getName().equals("getJSInjector"))
                        .findFirst()
                        .get();

                getJsInjector.setAccessible(true);
                var injector = getJsInjector.invoke(bridge);

                var getScriptString = Arrays.stream(injector.getClass().getDeclaredMethods())
                        .filter(method -> method.getName().equals("getScriptString"))
                        .findFirst()
                        .get();
                var scriptString = (String) getScriptString.invoke(injector);

                var allowedOrigins = Arrays.stream(bridge.getConfig().getAllowNavigation())
                        .filter(str -> str.contains("yourdomain.com") || str.contains("otherdomain.com"))
                        .filter(str -> str.contains("https://"))
                        // WebViewCompat likes things formatted particularly, trim trailing /*
                        .map(str -> str.replaceAll("/\\*$", ""))
                        .collect(Collectors.toSet());

                Logger.info("patchJSInjection", "Injecting custom rules " + allowedOrigins);
                WebViewCompat.addDocumentStartJavaScript(bridge.getWebView(), scriptString, allowedOrigins);
            }
        } catch (Exception e) {
            Logger.error( e.getMessage(), e);
        }
}

Thank you so much for this. I was thinking it had something to do with the JS code.
One mention: minSdkVersion will need to get bumped to 24, from the default 22. Just for the Java syntax sugar.

My allowedOrigins collection looks like this, as I have simple domain names in allowNavigation:

var allowedOrigins = Arrays.stream(bridge.getConfig().getAllowNavigation())
                        .map(str -> "https://" + str.replaceAll("^https?://|/\\*$", ""))
                        .collect(Collectors.toSet());

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug type: bug A confirmed bug report
Projects
None yet
8 participants