Android WebView: current issues and solutions

    At the last AndroidDevs Meetup meeting, several developers from the ICQ messenger team spoke. My talk was about Android WebView. For everyone who could not come to the meeting, I am publishing an article here based on the speech. I’ll go upstairs with big touches. I will not give deep technical details and a lot of code. If you are interested in the details, using the link at the end of the post you can download the application, specially written as an illustration, and see everything with examples.

    What is a webview?


    WebView is an Android platform component that allows you to embed web pages in Android applications. In fact, it is an embedded browser. Using WebView about a year ago, we decided to create an ICQ Web API for integrating web applications into our messenger. What is a web application? This is essentially an HTML page that contains JavaScript and works inside ICQ. Using the ICQ Web API, web pages via JavaScript can give ICQ different commands, for example, to send messages, open a chat, etc.



    Here's what it looks like in ICQ. From the Applications item, you can go to the list of applications. This is not yet a WebView, to get into it, you need to select one of the applications. Then we go directly to WebView, where the web application is downloaded from the network.

    How is this technically arranged? WebView has the ability to inject Java code in a certain way in JavaScript. JavaScript can call the code that we wrote and provided to it. This is the opportunity on which the entire ICQ Web API is based.



    Here it is shown that WebView works inside ICQ, there is an injected Java class between them, and applications from the network are loaded into WebView.

    So, JavaScript from WebView makes calls to ICQ Java code. There are a large number of different challenges, and during the development process, there were many problems associated with the operation of this mechanism, which I will discuss later.

    WebView Issues


    After starting the download, it is usually necessary to control this process: find out whether the download was successful, whether there were redirects, track the download time and other things. We will also talk about threads running JavaScript and calls in Java, about mismatch between Java and JavaScript types, Alerts behavior in JavaScript, and the size of the transmitted data. The solution to these problems will also be described later.

    WebView Basics


    In a nutshell about the basics of WebView. Consider four lines of code:

    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.loadUrl("http://www.example.com");
    webView.setWebViewClient(new WebViewClient() {…} );
    webView.setWebChromeClient(new WebChromeClient() {…} );
    

    It can be seen that we get a WebView and load example.com into it by calling WebView.loadURL (). There are two important classes in Android: WebViewClient and WebChromeClient, which interact with WebView. What are they needed for? WebViewClient is required to control the process of loading the page, and WebChromeClient is required to interact with this page after it is successfully loaded. WebViewClient is running until the page has finished loading, and then WebChromeClient is running. As you can see in the code, in order to interact with WebView, we need to create our own instances of these classes and pass them to WebView. Further, under certain conditions, WebView calls up various methods that we redefined in our instances, and so we learn about events in the system.

    The most important methods that WebView calls on the WebViewClient and WebChromeClient instances we created are:
    WebviewclientWebchromeclient
    onPageStarted ()openFileChooser (), onShowFileChooser ()
    shouldOverrideUrlLoading ()onShowCustomView (), onHideCustomView ()
    onPageFinished (), onReceivedError ()onJsAlert ()
    I will talk about the purpose of all these methods a little later, although much is already clear from the names themselves.

    Controlling page load in WebView


    After we gave the WebView a command to load the page, the next step is to find out the result of the execution: did the page load. From the point of view of the official Android documentation, everything is simple. We have a WebViewClient.onPageStarted () method that is called when the page starts loading. In the case of a redirect, WebViewClient.shouldOverrideUrlLoading () is called, if the page has loaded - WebViewClient.onPageFinished (), if it has not loaded - WebViewClient.onReceivedError (). Everything seems logical. How does this actually happen?

    EXPECTATION:
    1. onPageStarted → shouldOverrideUrlLoading (if redirected) → onPageFinished / onReceivedError

    REALITY:
    1. onPageStarted → onPageStarted → onPageFinished
    2. onPageStarted → onPageFinished → onPageFinished
    3. onPageFinished → onPageStarted
    4. onReceivedError → onPageStarted → onPageFinished
    5. onReceivedError → onPageFinished (no onPageStarted)
    6. onPageFinished (no onPageStarted)
    7. shouldOverrideUrlLoading → shouldOverrideUrlLoading

    In fact, everything is always different and depends on the specific device: onPageStarted (), onPageFinished () and other methods can be called twice, all methods can be called in a different order, and some may not be called at all. Especially often, such problems occur on Samsung and Google Nexus. This problem has to be solved by adding additional checks to our instance of the WebViewClient class. When it starts to work, we save the URL and then verify that the download occurs at that particular URL. If it is completed, then check for errors. Since the code is large, I will not cite it. I suggest that you look at it yourself in the example, the link to which will be at the end.

    JavaScript code injection


    Sample Java code:

    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.getSettings().setJavaScriptEnabled(true);
    mWebView.addJavascriptInterface(new MyJavaInterface(), "test");
    private class MyJavaInterface {
        @android.webkit.JavascriptInterface
        public String getGreeting() {
             return "Hello JavaScript!";
        }
    }
    

    JavaScript code example:


    Here's an example of injecting Java code in JavaScript. A short Java class, MyJavaInterface, is created, and it has one single getGreeting () method. Please note that this method is marked with the @JavaScriptInterface tagging interface - this is important. Calling the WebView.addJavascriptInterface () method, we throw this class into the WebView. Below we see how it can be accessed from JavaScript by calling test.getGreeting (). An important point here is the name test, which will later be used in JavaScript as an object through which you can make calls to our Java code.

    If we put a breakpoint on the return line “Hello JavaStript!” And look at the name of the stream in which the call was received, what stream will it be? This is not a UI thread, but a special Java Bridge thread. Therefore, if, when calling some Java methods, we want to manipulate the UI, then we need to make sure that these operations are transferred to the UI stream - use handlers or any other way.

    The second point: the Java Bridge stream cannot be blocked, otherwise JavaScript in WebView will simply stop working, and no user actions will have a response. Therefore, if you need to do a lot of work, tasks must also be sent to other threads or services.

    Java Type Mismatch in JavaScript


    When we call some methods written in Java and injected into JavaScript, as shown above, there is a problem with the mismatch between Java and JavaScript types. This table shows the basic mapping rules between type systems:
    Java -> JavaScriptJavaScript -> Java
    byte, short, char, int, long, float, doubleNumberNumberByte, short, int, long, float, double (not Integer, Byte, Short, Long, Float, Double and not char)
    booleanBooleanBooleanboolean (not boolean)
    Boolean, Integer, Long, Character, ObjectObjectArray, Object, Functionnull
    StringString (Object)StringString (not char [])
    char [], int [], Integer [], Object []undefinedundefinednull
    nullundefinednullnull

    The most important thing to notice here is that object wrappers are not passed. And of all Java objects in JavaScript, only String is mapped. Arrays and null in Java are converted to undefined in JavaScript.

    With passing in the opposite direction, from JavaScript to Java, there are also nuances. If you call some method that has elementary types as parameters, then you can pass number there. And if among the parameters of the method there are not elementary types, but, say, object wrappers, such as Integer, then such a method will not be called. Therefore, you need to use only elementary types of Java.

    Sizes of data transferred between Java and JavaScript


    Another major issue is the amount of data transferred between Java and JavaScript. If a sufficiently large amount of data (for example, pictures) is transferred from JavaScript to Java, then when an OutOfMemory error occurs, it will not work. The application just crashes. Here is an example of what can be seen in logcat in this case:

    Process com.estrongs.android.pop (pid 6941) has died
    Process com.google.android.youtube (pid 24613) has died
    Process kik.android (pid 3022) has died
    Process com.doeasyapps.optimize (pid 30743) has died
    Process com.sandisk.mz (pid 31340) has died
    WIN DEATH: Window{3fc4769b u0 mc.test/mc.test.MainActivity}
    WIN DEATH: Window{17a5c850 u0 mc.test/mc.test.DataSizeTestActivity}
    Process mc.test (pid 16794) has died
    Force removing ActivityRecord{215171e4 u0 mc.test/.DataSizeTestActivity t406}: app died, no saved state
    

    As you can see, if OutOfMemory happens in the application, then various other applications running on the device begin to fly out. As a result, having closed everything that is possible, Android reaches our application, and, since it is in foreground, closes it last. Once again, I want to remind you that we will not receive any exception, the application will simply crash. To avoid this, it is necessary to limit the size of the transmitted data. Much depends on the device. On some gadgets it turns out to transfer 6 megabytes, on some 2-3. For ourselves, we chose a limit of 1 megabyte, and this is enough for most devices. If you need to transfer more, then the data will have to be cut into chunks and transferred in parts.

    JavaScript Alerts


    By default, the Alert dialog in WebView does not work. If you load an HTML page with JavaScript and execute alert ('Hello'), then nothing will happen. To make it work, you need to define your WebChromeClient instance, override the WebChromeClient.onJSAlert () method and call super.onJSAlert () on it. This is enough for the Alerts to work.

    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.getSettings().setJavaScriptEnabled(true);
    webView.setWebChromeClient(new WebChromeClient() {
      @Override
      public boolean onJsAlert(....) {
           return super.onJsAlert(view, url, message, result);
      }
    }
    

    Handling a change in device orientation


    Another serious problem is portrait and landscape orientation. If you change the orientation of the device, then by default Activity will be recreated. In this case, all View, which are attached to it, will also be recreated. Imagine the situation: there is a WebView in which a certain game is loaded. The user reaches level 99, turns the device, and the WebView instance with the game is recreated, the page is reloaded, and he is again at the first level. To avoid this, we use manual handling of device configuration changes. In principle, this thing is known and described in the official documentation . To do this, just write the configChanges parameter in the AndroidManifest.xml in the activation section.


    This will mean that we ourselves are handling the change of orientation in activity. If the orientation changes, we get a call to Activity.onConfigurationChange () and we can change some resources programmatically. But usually only WebView itself, stretched to full screen, has activity with WebView, and there is nothing to do. It just redraws and everything continues to work fine. Thus, setting configChanges allows you not to recreate the Activity, and all the Views that are present in it will retain their state.

    Full screen media player


    If a media player is built into the web page, then there is often a need to ensure that it can work in full screen mode. For example, the youtube media player can work inside the web page in the iframe html tag , and it has a button for switching to full-screen mode. Unfortunately, this does not work in WebView by default. To make this work, you need to do a few manipulations. In the xml layout in which the WebView is located, we additionally place FrameLayout. This is a container that is stretched to full screen and in which the View with the player will be located:


    And then in your WebChromeClient instance, we override several methods:

    public void onShowCustomView(View v, CustomViewCallback c) {
          mWebView.setVisibility(View.GONE);
          mFullScreenContainer.setVisibility(View.VISIBLE);
          mFullScreenContainer.addView(view);
          mFullScreenView = v;
          mFullscreenViewCallback = c;
     }
     public void onHideCustomView() {
           mFullScreenContainer.removeView(mFullScreenView);
           mFullscreenViewCallback.onCustomViewHidden();
           mFullScreenView = null;
           mWebView.setVisibility(View.VISIBLE);
           mFullScreenContainer.setVisibility(View.GONE);
     }
    

    The system calls WebChromeClient.onShowCustomView () when the user clicks on the button for switching to full-screen mode in the player. onShowCustomView () accepts the View, which the player itself represents. This View is inserted into the FullScreenContainer and made visible, and the WebView is hidden. When the user wants to return from full screen mode, the WebChromeClient.onHideCustimView () method is called and the reverse operation is performed: display the WebView and hide the FullScreenContainer.

    Input type = ”file”


    Web developers know that this container is used on web pages so that the user can select a file and upload it to the server, or display it on the screen. For this container to work in WebView, we need to override the WebChromeClient.openFileChooser () method. In this method there is a certain callback to which you need to transfer the file selected by the user. It />does not have any additional functionality . The file selection dialog we need to provide. That is, we can open any standard Android picker in which the user selects the desired file, obtain it, for example, through onActivityResult (), and pass the openFileChooser () method to the callback.

    JavaScript code example:


    Sample Java code:

    WebChromeClient myClient = new WebChromeClient() {
      @SuppressWarnings("unused")
      public void openFileChooser(ValueCallback callback, String accept, String capture) {
         callback.onReceiveValue(Uri.parse("file://" + getFileFromSomeProvider()));
      }
    };
    WebView webView = (WebView) findViewById(R.id.web_view);
    webView.setWebChromeClient(myClient);
    

    Javascript Network Detection


    JavaScript has a useful Navigator object . It has an onLine field showing the status of the network connection. If we have a network connection, in the browser this field is true, otherwise false. For it to work correctly inside WebView, you must use the WebView.setNetworkAvailable () method. With it, we transmit the current network status, which can be obtained using the network broadcast receiver or in any other way that you track the network status in Android. This must be done constantly. If the network status has changed, then you need to call WebView.setNetworkAvailable () again and transfer the current data. In JavaScript, we will get the current value of this property through Navigator.onLine.

    Code examples


    github.com/mc-android-developer/mc.presentation.webview

    Questions and answers


    Question: There is a CrossWalk project - this is a third-party WebView implementation that allows you to use fresh Chrome on older devices. Do you have any experience, have you tried to embed it?
    Answer: I have not tried. At the moment, we support Android starting from the 14th version and we no longer focus on older devices.

    Question: How do you deal with artifacts that remain when drawing a WebView?
    Answer: We are not fighting with them, we tried - it did not work. This does not happen on all devices. We decided that this was not a glaring problem to spend more resources on.

    Question:Sometimes you need to embed a WebView in a ScrollView. This is ugly, but sometimes required by assignment. This is not encouraged, even prohibited somewhere, and after that there are shortcomings in the work. But still, sometimes it has to be done. For example, if you draw a WebView on top, and under it draw some kind of native component (which should be native according to the requirement), and all this should be done as a single ScrollView. That is, first the user would look at the whole page, and then, if he wanted, he would flip through to these native components.
    Answer: Unfortunately, I can not answer you, because I have not encountered such a situation. It is quite specific, and it is difficult for me to imagine the option when you need to put a WebView in a ScrollView.

    Question:There is a mail application. There is a hat on top with recipients and everything else. Even so, not everything will go smoothly. WebView has big problems when it tries to determine its size inside ScrollView.
    Answer: You can try to draw the indicated part of the UI inside the WebView.

    Question: That is, to completely transfer all the logic from the native part to the WebView and leave these containers?
    Answer: Even, maybe, you don’t need to transfer logic, I mean injecting Java classes. Logic can be left and called through the injected class. Only a UI can be ported to WebView.

    Question: You mentioned games in the messenger. Are they web applications?
    Answer:Yes, these are JavaScript web pages inside a WebView.

    Question: Do you do all this just to not rewrite the games natively?
    Answer: And for this too. But the main idea is to give third-party developers the opportunity to create applications that can be embedded in ICQ, and using this ICQ Web API to interact with the messenger.

    Question: That is, these games can also be played through a web browser on a laptop?
    Answer: Yes. It can be opened in a web browser, and sometimes we debug them right in it.

    Question: And if Intent, let’s say in Chrome to throw this toy, what problems then will be? If you do not write your own WebView, but use the services?
    Answer:The problem is that in our WebView we can provide the API through the injection of the Java class, and with this API the application will be able to directly interact with ICQ, send various commands to it. Let's say a command to get a username, to receive chats that he has open, send messages to chat directly from ICQ. That is, from Chrome, sending messages directly to ICQ will not work. In our case, all this is possible.

    Question: You mentioned that you cut data into pieces of one megabyte. How do you collect them later?
    Answer: We are not doing this right now, because we do not have such a need.

    Question: Enough for one megabyte?
    Answer:Yes. If the pictures are larger, then try to squeeze them. I said that if such a need exists, then this may be a solution - to cut and assemble later in Java.

    Question: How do you ensure the security of applications in the sandbox? Did I understand correctly that you need to call injected Java classes from JavaScript applications?
    Answer: Yes.

    Question: How will security be ensured in this case? Is access to any system functions prohibited?
    Answer:Right now, since the system is still quite young, we mainly use our own web applications, and we completely trust them. In the future, all applications that will come to us will be administered, the code will be viewed, a special Security Team is allocated for this. Additionally, a special permission system will be created, without which applications will not be able to access some critical information for the user.

    Also popular now: