Jak uzyskać wartość zwracaną z javascript w WebView Androida?

83

Chcę uzyskać wartość zwracaną przez JavaScript w systemie Android. Mogę to zrobić na iPhonie, ale nie mogę na Androidzie. Użyłem loadUrl, ale zwrócił on void zamiast obiektu. Czy ktoś może mi pomóc?

Cuong Ta
źródło

Odpowiedzi:

79

Taka sama, Keithale krótsza odpowiedź

webView.addJavascriptInterface(this, "android");
webView.loadUrl("javascript:android.onData(functionThatReturnsSomething)");

I zaimplementuj funkcję

@JavascriptInterface
public void onData(String value) {
   //.. do something with the data
}

Nie zapomnij usunąć onDataz proguardlisty (jeśli masz włączony proguard)

Kirill Kulakov
źródło
1
czy możesz rozwinąć tę proguardczęść?
Eugene
@eugene, jeśli nie wiesz, co to jest, prawdopodobnie go nie używasz. W każdym razie jest to coś, co można włączyć, co utrudnia inżynierię wsteczną aplikacji
Kirill Kulakov,
1
Dzięki. Wiem, że mam proguard-project.txt w katalogu głównym projektu, ale nic więcej. Pytam, ponieważ Twoje rozwiązanie działa, ale otrzymuję SIGSEGV. Oceniam, document.getElementById("my-div").scrollTopaby uzyskać pozycję przewijania z SwipeRefreshLayout::canChildScrollUp(). Podejrzewam, że może to być spowodowane zbyt częstym dzwonieniem w krótkim okresie. lub proguardco podejrzewam, bo po prostu nie wiem, co to jest ..
eugene
Powinna to być akceptowana odpowiedź, a nie wszystkie hacki komentowane tutaj
Romit Kumar
64

Oto hack, jak możesz to osiągnąć:

Dodaj tego klienta do swojego WebView:

final class MyWebChromeClient extends WebChromeClient {
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
            Log.d("LogTag", message);
            result.confirm();
            return true;
        }
    }

Teraz w wywołaniu javascript wykonaj:

webView.loadUrl("javascript:alert(functionThatReturnsSomething)");

Teraz w wywołaniu onJsAlert "wiadomość" będzie zawierać zwróconą wartość.

Felix Khazin
źródło
17
To niezwykle cenny hack; zwłaszcza gdy tworzysz aplikację, w której nie masz kontroli nad javascript i musisz radzić sobie z istniejącymi funkcjami. Zauważ, że możesz uzyskać ten sam wynik bez przechwytywania alertów JS za pomocą public boolean onConsoleMessage(ConsoleMessage consoleMessage)zamiast, onJsAlerta następnie w wywołaniu javascript, które robisz webView.loadUrl("javascript:console.log(functionThatReturnsSomething)");- w ten sposób pozostawiasz alerty JS w spokoju.
Mick Byrne,
Rozwiązanie WebChromeClient.onConsoleMessagewydaje się być czystsze, ale należy pamiętać, że nie działa z niektórymi urządzeniami HTC. Zobacz to pytanie, aby uzyskać więcej informacji.
Piotr
3
Czy jest jakiś powód, dla którego użycie addJavascriptInterface nie byłoby lepsze?
Keith
@Keith: Wymagałbyś obiektu, do którego javascript zapisuje wyniki, ale ponieważ nie możesz dotknąć istniejącego javascript w tym pytaniu, a istniejący js nie zapisuje do takiego obiektu, metoda interfejsu js tutaj nie pomaga ( tak to zrozumiałem).
Ray
1
@RayKoopa Niekoniecznie prawda. Chociaż możesz nie być w stanie edytować istniejącego kodu JavaScript na docelowej stronie internetowej, nadal możesz wywoływać metody na przekazanym obiekcie mWebView.addJavascriptInterface(YourObject mYourObject,String yourNameForObject);, wywołując funkcjęmWebView.loadUrl("javascript:yourNameForObject.yourSetterMethod(valueToSet)")
Chris - Jr
21

Służy addJavascriptInterface()do dodawania obiektu Java do środowiska Javascript. Niech JavaScript wywoła metodę na tym obiekcie Java, aby podać jego „wartość zwracaną”.

CommonsWare
źródło
jak mogę to zrobić w takiej sytuacji jak stackoverflow.com/questions/5521191/ ...
vnshetty
To nie pomoże, jeśli twój js nie zapisuje w tym obiekcie i nie możesz dotknąć js.
Ray
7
@PacMani: Użyj loadUrl("javascript:...")(Poziom API 1-18) lub evaluateJavascript()(Poziom API 19+), aby ocenić własny JavaScript w kontekście aktualnie załadowanej strony internetowej.
CommonsWare
@CommonsWare: Dzięki, rozumiem;) jednak menedżer zdecydował, że js można modyfikować przez „nadużywanie alertów javascript” (eyeroll), więc udało mi się to za pomocą metody interfejsu.
Ray,
12

Oto, co dziś wymyśliłem. Jest bezpieczny dla wątków, rozsądnie wydajny i umożliwia synchroniczne wykonywanie JavaScriptu z Java dla Android WebView .

Działa na Androidzie 2.2 i nowszych. (Wymaga commons-lang, ponieważ potrzebuję fragmentów kodu przekazywanych do eval () jako ciągu JavaScript. Możesz usunąć tę zależność, zawijając kod nie w cudzysłów, ale w function(){})

Najpierw dodaj to do swojego pliku Javascript:

function evalJsForAndroid(evalJs_index, jsString) {
    var evalJs_result = "";
    try {
        evalJs_result = ""+eval(jsString);
    } catch (e) {
        console.log(e);
    }
    androidInterface.processReturnValue(evalJs_index, evalJs_result);
}

Następnie dodaj to do swojej aktywności w Androidzie:

private Handler handler = new Handler();
private final AtomicInteger evalJsIndex = new AtomicInteger(0);
private final Map<Integer, String> jsReturnValues = new HashMap<Integer, String>();
private final Object jsReturnValueLock = new Object();
private WebView webView;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    webView = (WebView) findViewById(R.id.webView);
    webView.addJavascriptInterface(new MyJavascriptInterface(this), "androidInterface");
}

public String evalJs(final String js) {
    final int index = evalJsIndex.incrementAndGet();
    handler.post(new Runnable() {
        public void run() {
            webView.loadUrl("javascript:evalJsForAndroid(" + index + ", " +
                                    "\"" + StringEscapeUtils.escapeEcmaScript(js) + "\")");
        }
    });
    return waitForJsReturnValue(index, 10000);
}

private String waitForJsReturnValue(int index, int waitMs) {
    long start = System.currentTimeMillis();

    while (true) {
        long elapsed = System.currentTimeMillis() - start;
        if (elapsed > waitMs)
            break;
        synchronized (jsReturnValueLock) {
            String value = jsReturnValues.remove(index);
            if (value != null)
                return value;

            long toWait = waitMs - (System.currentTimeMillis() - start);
            if (toWait > 0)
                try {
                    jsReturnValueLock.wait(toWait);
                } catch (InterruptedException e) {
                    break;
                }
            else
                break;
        }
    }
    Log.e("MyActivity", "Giving up; waited " + (waitMs/1000) + "sec for return value " + index);
    return "";
}

private void processJsReturnValue(int index, String value) {
    synchronized (jsReturnValueLock) {
        jsReturnValues.put(index, value);
        jsReturnValueLock.notifyAll();
    }
}

private static class MyJavascriptInterface {
    private MyActivity activity;

    public MyJavascriptInterface(MyActivity activity) {
        this.activity = activity;
    }

    // this annotation is required in Jelly Bean and later:
    @JavascriptInterface
    public void processReturnValue(int index, String value) {
        activity.processJsReturnValue(index, value);
    }
}
Keith
źródło
2
Dziękuję za powyższy kod - zaadoptowałem go w moim projekcie na Androida. Jest w tym jednak pułapka, wynikająca ze złego zaprojektowania Java API. Wiersz: jsReturnValueLock.wait (waitMs - (System.currentTimeMillis () - start)); Czasami może się zdarzyć, że wartość argumentu funkcji wait () wynosi 0. A to oznacza „czekaj nieskończenie” - od czasu do czasu otrzymywałem błędy „brak odpowiedzi”. Rozwiązałem to, sprawdzając: if (elapsed> = waitMS) przerwa; i nie wywołując ponownie System.currentTimeMillis () poniżej, ale używając wartości elapsed. Najlepiej byłoby edytować i naprawić powyższy kod.
gregko
Wow, wielkie dzięki! Widziałem, że to też zawisło i nigdy nie zorientowałem się, skąd pochodzi.
Keith
1
Ale jeśli uruchomię evalJS w odbiorniku button.click itp. waitForJsReturnValue może spowodować zawieszenie się funkcji evalJs (). Więc webView.loadUrl () wewnątrz handler.post () może nie mieć szansy na wykonanie, ponieważ odbiornik button.click nie zwrócił.
victorwoo
Ale jak zawijać kod nie w cudzysłowach, ale w function(){}? Jak umieścić jsString w {}?
victorwoo
7

W przypadku API 19+ najlepszym sposobem na to jest wywołanie compareJavascript w swoim WebView:

webView.evaluateJavascript("foo.bar()", new ValueCallback<String>() {
    @Override public void onReceiveValue(String value) {
        // value is the result returned by the Javascript as JSON
    }
});

Powiązana odpowiedź z bardziej szczegółowymi informacjami: https://stackoverflow.com/a/20377857

David
źródło
5

Rozwiązanie, które zasugerował @Felix Khazin, działa, ale brakuje jednego kluczowego punktu.

Wywołanie javascript należy wykonać po WebViewzaładowaniu strony internetowej w . Dodaj to WebViewClientdo WebView, wraz z WebChromeClient.

Pełny przykład:

@Override
public void onCreate(Bundle savedInstanceState) {
    ...
    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.setWebViewClient(new MyWebViewClient());
    webView.setWebChromeClient(new MyWebChromeClient());
    webView.loadUrl("http://example.com");
}

private class MyWebViewClient extends WebViewClient {
    @Override
    public void onPageFinished (WebView view, String url){
        view.loadUrl("javascript:alert(functionThatReturnsSomething())");
    }
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return false;
    }
}

private class MyWebChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    Log.d("LogTag", message);
        result.confirm();
        return true;
    }
}
Mitko Katsev
źródło
1

Jako alternatywny wariant korzystający ze schematu niestandardowego:

Główna aktywność

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WebView.setWebContentsDebuggingEnabled(true);
        }

        final WebView webview = new CustomWebView(this);

        setContentView(webview);

        webview.loadUrl("file:///android_asset/customPage.html");

        webview.postDelayed(new Runnable() {
            @Override
            public void run() {

                //Android -> JS
                webview.loadUrl("javascript:showToast()");
            }
        }, 1000);
    }
}

CustomWebView

public class CustomWebView extends WebView {
    public CustomWebView(Context context) {
        super(context);

        setup();
    }

    @SuppressLint("SetJavaScriptEnabled")
    private void setup() {
        setWebViewClient(new AdWebViewClient());

        getSettings().setJavaScriptEnabled(true);

    }

    private class AdWebViewClient extends WebViewClient {

        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {

            if (url.startsWith("customschema://")) {
                //parse uri
                Toast.makeText(CustomWebView.this.getContext(), "event was received", Toast.LENGTH_SHORT).show();

                return true;
            }


            return false;
        }

    }
}

customPage.html (znajduje się w zwiniętych zasobach)

<!DOCTYPE html>
<html>
<head>
    <title>JavaScript View</title>

    <script type="text/javascript">

        <!--JS -> Android-->
        function showToast() {
            window.location = "customschema://goto/";
        }

    </script>
</head>

<body>

</body>
</html>
yoAlex5
źródło
0

Możesz to zrobić w ten sposób:

[Activity(Label = "@string/app_name", Theme = "@style/AppTheme", MainLauncher = true)]
public class MainActivity : AppCompatActivity
{
    public WebView web_view;
    public static TextView textView;

    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
        Xamarin.Essentials.Platform.Init(this, savedInstanceState);

        Window.AddFlags(WindowManagerFlags.Fullscreen);
        Window.ClearFlags(WindowManagerFlags.ForceNotFullscreen);

        // Set our view from the "main" layout resource
        SetContentView (Resource.Layout.main);

        web_view = FindViewById<WebView>(Resource.Id.webView);
        textView = FindViewById<TextView>(Resource.Id.textView);

        web_view.Settings.JavaScriptEnabled = true;
        web_view.SetWebViewClient(new SMOSWebViewClient());
        web_view.LoadUrl("https://stns.egyptair.com");
    }

    public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
    {
        Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);

        base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

public class SMOSWebViewClient : WebViewClient
{
    public override bool ShouldOverrideUrlLoading(WebView view, IWebResourceRequest request)
    {
        view.LoadUrl(request.Url.ToString());
        return false;
    }

    public override void OnPageFinished(WebView view, string url)
    {
        view.EvaluateJavascript("document.getElementsByClassName('notf')[0].innerHTML;", new JavascriptResult());
    }
}

public class JavascriptResult : Java.Lang.Object, IValueCallback
{
    public string Result;

    public void OnReceiveValue(Java.Lang.Object result)
    {
        string json = ((Java.Lang.String)result).ToString();
        Result = json;
        MainActivity.textView.Text = Result.Replace("\"", string.Empty);
    }
}
Sherif Riad
źródło