Mikael
Tennhammar

Native Web app on Android, Part II

Senior System Developer

How smart can ’IT’ be?


In previous part, I show how to set up so that Javascript code could call android code, now let's take a look how you can communicate the other way around, so that we actually can have events coming from the android platform affecting the current state of the loaded web application, to show this and to get some realtime events from something that would be hard to do from the Javascript code, we set up to monitoring your WIFI connection and show that on the web page.

 

The web application

First lets combined the two html files from previous example to one index.html, and add local navigation functionality, simply hide and unhide the two buttons. Still the java function is called, but it will not be used anymore for navigation but only to show the Toast.

<html>

   <head>

       <Script>

      function onJavaScriptAction(action) {

           var forward = document.getElementById("forward");

           var back = document.getElementById("back");

            if (action === "goForward") {

               forward.style.display = 'none'

               back.style.display = 'block'

           } else if (action === "goBack") {

               forward.style.display = 'block'

               back.style.display = 'none'

           }

           jsWrapper.onJavaScriptAction(action);

       };

</script>

</head>

<body>

            <div style="padding-top: 150px;">

   <div style="padding-left: 100px" >

                         <div id="forward" style="display: block;">

                         <p onClick="onJavaScriptAction('goForward')">

                                     <button >Click Me to advance</button>

                         </p>

                         </div>

<div id="back" style="display: none;">

                                     <p onClick="onJavaScriptAction('goBack')">

                                                 <button>Go back</button>

                                     </p>

</div>

</div>

   </body>

</html>

 

Communication from Android

 

To send events from Android I first declare a javascript function, this function will be called from the Java code later on, add the function to the <script> tag of the index.html file.

         function onAndroidWifiStatusEvent(name, level) {

           var nameElement = document.getElementById("wifi-name");

            var levelElement = document.getElementById("wifi-level");

            nameElement.innerHTML = name + ":";

            levelElement.innerHTML = level + "/5";

            return "ok";

        };

Then add an extra <div> element in the top most <div style=”padding…, still in the index.html, this will be used to show the current WIFI status in the UI.

<div>

   <div style="float:left;padding-left:75px;padding-right:5px;display:inline" id="wifi-name">Wifi:</div>

   <div id="wifi-level">5/5</div>

</div>

 

Android application

 

The way to send messages to the web application is to invoke javascript code that will execute the code in the webview’s context. First I create a static string that holds the structure of the javascript code that will be executed, so add this in the MainActivity.java.

At the same time also add the constant for the new html file.

private static final String JAVA_SCRIPT = "javascript:try{ onAndroidWifiStatusEvent('%s',%d)}catch(error){jsWrapper.onError(error.message);}";

private static final String INDEX_HTML_FILE = "file:///android_asset/index.html";

And then create a method that will parameterized the javascript code and execute it in the webview.

private void onAndroidWifiEvent(String ssid, int level) {

  String jsCmd = String.format(JAVA_SCRIPT, ssid, level);

  webView.evaluateJavascript(jsCmd, new ValueCallback<String>() {

   @Override

   public void onReceiveValue(String value) {

       Log.i(TAG, "JS response " + value);

   }

});

Note that the jsWrapper.onError in the catch statement will call the onError method in JSWrapper and any errors will be logged in the Android application context, that could be very helpful when debugging. The onReceiveValue will display any return values from the Javascript function.

Then I update the JSWrapper with an onError method and remove the Navigator from the previous example. Change the creation in MainActivity.java to not include the anonymous Navigator any more.

public class JSWrapper {

   private static final String TAG = JSWrapper.class.getSimpleName();

   private Context context; 

   public JSWrapper(Context context) {

       this.context = context;

   }

   @JavascriptInterface

   public void onError(String error) {

       Log.e(TAG, "JSError: " + error);

   }

   @JavascriptInterface

   public void onJavaScriptAction(String action){

       Toast.makeText(context, "JS says: " + action, Toast.LENGTH_SHORT).show();

   }

}

 

The WIFI monitoring

 

Add thees to the AndroidMainfest.xml, shall be in the <manifest> tag this is to allow the application to monitoring the WIFI. To monitor the WIFI in android you subscribe on intent.

<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

To subscribe on the changes of the WIFI connection we need a BrodcastReceiver that can subscribe on the system intents from the ConnectionManager. Create a class called WifiStateListener.java with the following implementaion.

public class WifiStateListener extends BroadcastReceiver {

   private final static String TAG = WifiStateListener.class.getSimpleName();

   public static final String ON_WIFI_STATUS_CHANGE = "ON_WIFI_STATUS_CHANGE";

   public static final String WIFI_NAME = "WIFI_NAME";
   public static final String WIFI_LEVEL = "WIFI_LEVEL";

   public static void GetCurrentState(Context context) {

       WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
       ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
       NetworkInfo networkInfo = connectivityManager.getActiveNetworkInfo();
       broadcastStatus(context, networkInfo, wifiManager);
   }
 

   @Override
   public void onReceive(Context context, Intent intent) {

       WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
       NetworkInfo networkInfo = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
       broadcastStatus(context, networkInfo, wifiManager);
   }

   private static void broadcastStatus(Context context, NetworkInfo networkInfo, WifiManager wifiManager) {

       if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) {
           //get the different network states
          
if (networkInfo.getState() == NetworkInfo.State.CONNECTED) {
               // Level of current connection
              
int rssi = wifiManager.getConnectionInfo().getRssi();
               String ssid = wifiManager.getConnectionInfo().getSSID();
               int level = WifiManager.calculateSignalLevel(rssi, 5);

               Intent newIntent = new Intent(ON_WIFI_STATUS_CHANGE);
                    // You can also include some extra data
             
newIntent.putExtra(WIFI_NAME, ssid);
               newIntent.putExtra(WIFI_LEVEL, level);
               LocalBroadcastManager.getInstance(context).sendBroadcast(newIntent);
           }
       }
     }
  }

The class has two public functions one static, GetCurrentState, that can be called to get the current status, and one onReceive which should be called when the network status is changed. Both function will use the LocalBroadcastManager to distribute the status.

In the AndroidManifest.xml add the following lines on the same level as the <activity> tag, to make Android call the onReceive function whenever there is a network status change

<receiver android:name=".WifiStateListener">
            <intent-filter>
                         <action android:name="android.net.wifi.supplicant.CONNECTION_CHANGE" />                           <action android:name="android.net.wifi.STATE_CHANGE" />
            </intent-filter>
</receiver>

In order to receive the status in MainActivity.java  we need a register a BroadcastReceiver that handles those Intents from the WifiStateListener. Add a private member in the MainActivity

private BroadcastReceiver wifiStatusReceiver;

construct it as an anonymous class that handles the intent in onCreate in MainActivity.java

wifiStatusReceiver = new BroadcastReceiver() {

   @Override

   public void onReceive(Context context, Intent intent) {

       String ssid = intent.getStringExtra(WifiStateListener.WIFI_NAME);

       int level = intent.getIntExtra(WifiStateListener.WIFI_LEVEL,0);

       onAndroidWifiEvent(ssid, level);

   }

};

register it as a BroadcastReceiver with the LocalBroadcastManager still in onCreate

LocalBroadcastManager.getInstance(this).registerReceiver(wifiStatusReceiver, new IntentFilter(WifiStateListener.ON_WIFI_STATUS_CHANGE));

And finally for the completeness sake un register onDestroy in MainActivity

public void onDestroy() {
   LocalBroadcastManager.getInstance(this).unregisterReceiver(wifiStatusReceiver);
   super.onDestroy();
}

 

Security issues

 

Something to think about is that you should probably not browse away in your web view and load pages where you do not have control over what code is running. If you need to open another sites page you should probably better open that link in one of the phone’s real browsers.

To get control over page loading and only allow local content you can add a web client to your web view and implement the method to get.

webView.setWebViewClient(new WebViewClient() {

   @Override

   public boolean shouldOverrideUrlLoading(WebView view, String url) { 

       if (Uri.parse(url).getScheme().equals("file")) {

           // This is my web site, so do not override; let my WebView load the page

           Log.i(TAG, Uri.parse(url).getScheme() + ": " + Uri.parse(url).getHost() + ":" + Uri.parse(url).getPath());

           return false;

       }

       // Otherwise, the link is not for a page on my site, so launch another Activity that handles URLs

       Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));

       startActivity(intent);

       return true;

   } 

@Override

   public void onPageFinished(WebView view, String url) {

       super.onPageFinished(view, url);

       WifiStateListener.GetCurrentState(MainActivity.this);

   }

});

Last I also added the method onPageFinished which is a perfect place to call for the WifiStateListener for the initial state.