Adding a done button to Android’s InAppBrowser

Hello guys. This one is going to be a mobile app development related post again. I came across an use case where I had to create a done button in the Android InAppBrowser, just like how we have it in iOS. It required some work to get it done, so thought I should share it with you guys too. Let’s see how to do it.

First, let me get the problem straight. This is how the InAppBrowser appears on Android and iOS.

Screen Shot 2016-01-23 at 8.07.54 pm

As you can see, iOS has a neat done button at the bottom which the user can press to close the InAppBrowser after he is done using it, whereas in the case of Android, the user has to either press the cross next to the location bar or use the back button to close the InAppBrowser. We’ll try to recreate this done button on Android too.

You might be asking why can’t we just add the button to the source of the page that we are opening, by injecting it using javascript or even more easy, have it declared in the page that we are opening itself??? The reason being, in most of the use cases, you will be opening a third party site like Facebook or Youtube, rather than the site that you create, in which case the technique that we are going to learn will be very useful.

Use case

The use case we’ll be working on is this. The user opens Youtube in the InAppBrowser. After he has done using it, he’ll be using the done button to close the InAppBrowser. This ensures that the user has to go through the same procedure to close the InAppBrowser in both iOS and Android.

I’ll be using Ionic as the hybrid mobile app framework. For simplicity I’ll ignore using Angular for now.

Create a Blank Mobile App

First of all let’s create a blank ionic app.

ionic start InAppBrowser blank

Now we have created a blank Ionic app. Also make sure to add your required platforms. I’ll be adding Android and iOS.

ionic platform add android
ionic platform add ios

Install InAppBrowser

Next, we have to install the InAppBrowser plugin. To do so, type the following command:

cordova plugin add cordova-plugin-inappbrowser

You can find the repository for the plugin here. Now we have added the InAppBrowser plugin.

Understanding InAppBrowser

Before we start with the implementation, let us understand the basics of InAppBrowser. An InAppBrowser allows a mobile application to open web pages within the app without the user having to leave the app. If there was no InAppBrowser the page would open in the mobile’s default browser instead.

Opening a web page in the InAppBrowser is very simple. We just have to call window.open (Of course, after installing the InAppBrowser plugin).

var win = window.open( "http://google.com", "_blank", "location=yes" );
win.addEventListener( "loadstop", function() {
    //code goes here
    win.executeScript({ 
        code: "localStorage.setItem('name', '')" 
    });
});

The first argument is the web page to be opened, the second in this case is _blank, which ensures that the page is always opened in a separate tab. The third is a list of configuration options. We can use these options to do a lot of things, like deciding whether to show the address bar or not and so on. You can have a look at the documentation here.

It has an addEventListener method, but it only supports the following four events :

  • loadstart – event fires when the InAppBrowser starts to load a URL.
  • loadstop – event fires when the InAppBrowser finishes loading a URL.
  • loaderror – event fires when the InAppBrowser encounters an error when loading a URL.
  • exit – event fires after the InAppBrowser window is closed.

In the above code, we have added a listener to the loadstop event. In the loadstop callback function, we make use of a function called executeScript to execute some code.

executeScript

executeScript is a method that executes javascript in the context of the InAppBrowser. The executeScript method has two parameters. The first parameter is an object which can contain one of the two possible key-value pairs:

code – a string representing the script to be executed
file – a string representing the name of the file containing the script to be executed

The second parameter is a callback function, the first argument of which is an array containing the return value of the scripts executed as the first parameter. Consider the following example:

    win.executeScript({
      code: "(function() { return 'sidthesloth' })()"
    },
    function(values) {
        //the first element of the array contains the value returned by the script
        var name = values[0];
    });

When the above code is executed, the value returned by the function will be available as the first element of the array argument to the callback function, in this case value[0] will be ‘sidthesloth’.

insertCSS

insertCSS is another method that can be used to inject CSS in the InAppBrowser’s window. The insertCSS method has two parameters. The first parameter is an object which can contain one of the two possible key-value pairs:

code – a string representing the CSS to be injected
file – a string representing the name of the file containing the CSS to be injected

Consider the following example:

win.insertCSS({
    "code": ".body { background: rgba(0, 0, 0, 0.8); }"
});

We will be making use of these two functions to create our button.

Base template

The base template will just have a button that will launch the InAppBrowser.

<!DOCTYPE html>
<html>
 <head>
 <title>Sharing Data between App and InAppBrowser</title>
 </head>
 <body ng-app="starter">
 <ion-pane>
 <ion-header-bar class="bar-stable">
 <h1 class="title">Ionic Blank Starter</h1>
 </ion-header-bar>
 <ion-content>
    <button type="button" onclick="openInAppBrowser()">Open In App Browser</button>
 </ion-content>
 </ion-pane>
 < script type="text/javascript">
    function openInAppBrowser() {
        var win = window.open( "test.html", "_blank", "location=yes" );
    }
 </ script >
 </body>
</html>

Let’s us inspect the openInAppBrowser function to see what is being done.

function openInAppBrowser() {
    var win = window.open('http://youtube.com', '_blank', 'location=no');

    win.addEventListener('loadstop', function() {
        //code to inject CSS and JS
    })
}

We open the InAppBrowser, using the window.open function, passing in the URL to be opened as youtube.com and _blank to ensure that the site opens in a new tab. We’ll pass in a configuration parameter location=no to hide the location bar in Android. This also ensures that in Android, users will not be able to use the cross next to the location bar to close the InAppBrowser.

Note: The location bar does not appear in iOS irrespective of the value of this parameter, as it is Android only.

We’ll then attach a loadstop event, to inject the CSS and execute the required scripts.

We saw earlier that both the injectCSS and executeScript method take two options, one is code which allows the code/style to be passed as a string, whereas the other one is file which allows either a .js or .css file to be passed.

But we cannot use the file option when we are loading a third party site. The reason is because, when the file option is used, all that the insertCSS/executeScript function does is that, it injects link or script tags into the body of the opened html page. This means a request will be made to load the .css/.js files from the site that we opened in the InAppBrowser (i.e) in this case a request will be made from Youtube to load a file which we have in our computer, which is forbidden, as it is a cross-origin request.

So we have to sadly stick with the code option for now..:(. When using the code option, the style tag will be used to inject inline CSS and script tags will be used to inject inline JS. Yeah I know it is bad but that’s the only choice we have. Having said that, if it’s a site that you are developing, which is opened in the InAppBrowser, then you can use the file option.

Injecting the CSS

We’ll make use of the insertCSS method to inject the required CSS.

.youtube_done_button { 
    position: fixed; 
    bottom: 0; 
    width: 100%; 
    background: rgba(0, 0, 0, 0.8); 
    color: #2196F3; 
    padding: 10px; 
    font-size: 20px;
}

The above code applies style to the done button to make it appear similar to the button that appears on iOS.
Inside the loadstop event callback, inject the CSS using the insertCSS function.

win.insertCSS({
    "code": ".youtube_done_button { position: fixed; bottom: 0; width: 100%; background: rgba(0, 0, 0, 0.8); color: #2196F3; padding: 10px; font-size: 20px;}"
});

Creating the done button in JS

Next, we will inject the JS code to create a button

(function () { 
    var body = document.querySelector('body'); 
    var button = document.createElement('div'); 
    button.innerHTML = 'Done'; 
    button.classList.add('youtube_done_button'); 
    button.onclick = function () { 
        localStorage.setItem('close', 'true'); 
    }; 
    body.appendChild(button); 
})();

The above code creates a button and attaches it to the body of the page opened in the InAppBrowser. It also attaches an onclick listener to it.

Inside the loadstop event callback, add the following code:

win.executeScript({
    code: "(function() { var body = document.querySelector('body'); var button = document.createElement('div'); button.innerHTML = 'Done'; button.classList.add('youtube_done_button'); button.onclick = function() { localStorage.setItem('close', 'true'); }; body.appendChild(button); })();"
});

You might be wondering why are we setting a value with key close in localStorage instead of just closing the InAppBrowser. But we can’t do that…:(

The reason being that in Javascript, only scripts that opened a window are allowed to close it. This means that, in this case, only the parent window which has a reference to the opened child window in the variable win can close it.

However, the child window does not have a reference to the win variable and hence it cannot close itself.

In javascript, whenever a function is defined, a scope is created at the level where it is defined and is made available to it. We make use of this fact. The callback function to the setInterval function is defined in the scope of the openInAppBrowser function.

As the win variable is defined inside the openInAppBrowser function, the callback function has access to the win variable. Now we can use win.close to close the InAppBrowser. But we don’t know when the user clicks on the close button. This is what we are going to solve.

function openInAppBrowser() {
    var close;
    var closeLoop;
            
    var win = window.open('http://youtube.com', '_blank', 'location=no');

    win.addEventListener('loadstop', function() {
        //injecting the CSS
        win.insertCSS({
            "code": ".youtube_done_button { position: fixed; bottom: 0; width: 100%; background: rgba(0, 0, 0, 0.8); color: #2196F3; padding: 10px; font-size: 20px;}"   
        });
  
        //setting close to false when the InAppBrowser is opened
        win.executeScript({
            code: "localStorage.setItem('close', 'false');"
        });
             
        //creating and attaching a button with click listener to the opened page
        win.executeScript({
                code: "(function() { var body = document.querySelector('body'); var button = document.createElement('div'); button.innerHTML = 'Done'; button.classList.add('youtube_done_button'); button.onclick = function() { localStorage.setItem('close', 'true'); }; body.appendChild(button); })();"                
        });
                
        closeLoop = setInterval(function() {
            win.executeScript({
                code: "localStorage.getItem('close');"
            },
            function(values) {
                close = values[0];
                if (close == "true") {
                     clearInterval(closeLoop);
                     win.close();
                }
            });
        }, 1000);
    })
}

Here, we declare two variables close and closeLoop. First, we set the close value in local storage to false every time the InAppBrowser is opened.

Then we inject a button with a click listener and it’s associated CSS. When this button is clicked, it sets the close local storage variable to true.

We also declare a function that executes at an interval of one second. This function gets the value with key close from local storage. We then check if it is true, if so, we proceed to clear the interval and close the window. If it is false, then we do nothing.

When the user opens the InAppBrowser and is done using it, he clicks the close button which we have injected. The button sets the close local storage variable to true. On the next execution of the interval function, the value returned by the close local storage variable will be true, and we proceed to close the InAppBrowser.

Note: We do not have access to win inside the onclick of the button as it is injected dynamically into the opened page as inline JS using script tag (i.e) in other words, it has access only to the child window context.

The interval of one second is used to ensure that if the user closes the InAppBrowser and opens it again, there is enough time for the code:

win.executeScript({
    code: "localStorage.setItem('close', 'false');"
});

to execute and reset the code variable in local storage to false. This ensures that the interval function executes only after the variable code in local storage has been reset to false, thus avoiding accidental closing of the InAppBrowser.

That’s about it to creating your own close button in Android’s InAppBrowser. It might seem that we are going a little overboard to achieve this, but if you ask me it’s worth it, because it ensures that the user has to remember only one way of closing the InAppBrowser. See you in the next one guys…Peace..:)

7 thoughts on “Adding a done button to Android’s InAppBrowser

  1. It didnt work for me, i m not getting any error though, but “Done” button is not displayed. I am using genymotion as a simulator.

    Like

  2. It works. Can use localstorage storage event to instead of polling. angular.element($window).on(‘storage’, function (event) {
    if (event.key === ‘close’) {
    ref.executeScript({
    code: “localStorage.getItem(‘close’);”
    },
    function (values) {
    if (values[0] === “true”) {
    angular.element($window).off(‘storage’);
    ref.close();
    }
    });
    }
    });

    Liked by 1 person

  3. Thanks for the really nice explanation! It works for me. Had to make a small change though. The line “button.classList.add(‘youtube_done_button” needs a closing inverted comma, bracket and semicolon.

    Like

  4. Hm, something strange is happening. If while in the browser I open a link, go back, hit the close then open the page again, it goes directly to win.close. Ideas?

    Like

    • Just remove the item from the local store, when you are closing

      if (close == “true”) {
      //setting close to false when the InAppBrowser is opened
      that. win.executeScript({
      code: “localStorage.removeItem(‘close’);”
      });
      clearInterval(closeLoop);
      that. win.close();
      }

      Like

  5. Thank you very much, it works and solved me a lot of problems.
    Please fix error in code as also described by Vanitha.
    Replace this string “button.classList.add(‘youtube_done_buttonbutton.onclick” with this “button.classList.add(‘youtube_done_button’); button.onclick”

    Like

Leave a comment