Building an app for network capability

This image has an empty alt attribute; its file name is AlterMundiInicio.png

Building an app for network capability

Hi! I’m Tomás. This post is a brief of the work that we did in the last few weeks. The prototype of a network capability app was achieved, and we’re starting to test it on communities. The app is still a prototype: it has only three functions (connect to a webpage using the WiFi, check if you’re in a LibreMesh network, and check the private IP of the device) and the front-end consists of only these three buttons, but it has now all the logic that was needed to start working on the rest of the app.

Basic functions

The first approach was to check if the user was able to connect to the LibreMesh local address by checking it with a ping, and then we decided to move forward to an HTTP GET instead. With this idea in mind, we prepared a new version of the application that sends a command to the device (a curl command) instead of a Java method with a previously developed android interface (for the ping version).

public boolean httpGetToLibreMesh() throws InterruptedException, IOException {
    //FIXME: modificar google por la IP de LibreMesh
    String[] cmdLine = {"sh", "-c", "curl --head --silent --fail google.com"};
    Process p1 = java.lang.Runtime.getRuntime().exec(cmdLine);
    int returnVal = p1.waitFor();
    return returnVal == 0;
}

This simple code solves the problem. It returns true if the HTTP GET to google.com worked, and false if it didn’t. It can be easily modified with the LibreMesh IP Address.

The next objective was to inform the user if the device wasn’t connected to the WiFi. In order to do so, we have to get the WifiManager from the ApplicationContext, and then check if the wifi is working.

public boolean verifyLibreMeshConnection() {
    WifiManager wm = (WifiManager) getApplicationContext().getSystemService(WIFI_SERVICE);
    if(wm.isWifiEnabled()) {
        return (wm.getConnectionInfo().getNetworkId() == -1) ? false : true;
    }
    return false;
}

Then, we needed a web navigator (WebView) inside the app with the capability to run the LibreMesh router website (On the first approach, to a google.com website).

Using a WebView object with the shouldOverrideUrlLoading overridden we can show a webpage in the app without the requirement of showing an external navigator (Android provides the Android WebView App that does this inside the LibreMesh app).

So with this simple code, we can configure the WebView to enter to a site inside the app.

WebView navegador;
navegador = (WebView) findViewById(R.id.navegadorLibreMesh);
navegador.setWebViewClient(new WebViewClient() {

@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
    view.loadUrl(url);
    return true;
}
});
navegador.loadUrl("http://www.google.com");

Choosing through which network interface to send data to

Once having the WebView, the next step was to control through which network interface the application sends the network requests. In order to do that we have to access the ConnectivityManager. It was created as a class variable and defined on the function “onCreate” of the activity that holds the WebView. The connectivityMaganer isn’t a new instance but a reference to the object that controls the connections in the context of the App.

connectivityManager = (ConnectivityManager) getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);

Then we needed a function that can request to use the WiFi. The idea is to make a NetworkRequest and send it to the connectivityManager, but it also needed a NetworkCallback to specify what to do when the Network was available to accomplish the request. So as a second parameter of the request there’s an anonymous class that overrides the methods needed.

@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void requestWifi() {
    final NetworkRequest networkRequest = new NetworkRequest.Builder()
           .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
           .build();

    connectivityManager.requestNetwork(networkRequest, new ConnectivityManager.NetworkCallback() {
        @Override
        public void onAvailable(Network network) {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
                connectivityManager.bindProcessToNetwork(network);
            else
                ConnectivityManager.setProcessDefaultNetwork(network);
        }

        @Override
        public void onLost(Network network) {
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
                connectivityManager.bindProcessToNetwork(null);
            else
                ConnectivityManager.setProcessDefaultNetwork(null);
        }

        @Override
        public void onUnavailable() {
            super.onUnavailable();
        }
    });
}

The last thing that I needed to do was a function that runs the WebView.

private void iniciarNavegador() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) requestWifi();

    WebView navegador;
    navegador = (WebView) findViewById(R.id.navegadorLibreMesh);
    navegador.setWebViewClient(new WebViewClient() {

       @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    });
    navegador.loadUrl("192.168.0.2");
}

Getting the LibreMesh address

The next step was to move forward with getting the LibreMesh IP address. On the other hand, that’s not more than just an algorithm or a gateway, either way, does the same results. This can give us an alternative way to see if the user is connected or not to the LibreMesh server (we get the IP through the algorithm and compare the gateway version).

The idea was pretty simple and only required a int to ip auxiliar function. So we decided to collect all the methods that returned wifi information and send them to a new WifiInformationManager class. So, this class sends all the information that we need from the WiFi:

public class WifiInformationManager extends AppCompatActivity {
    private static String intToIp(int addr) {
        return  ((addr & 0xFF) + "." +
                ((addr >>>= 8) & 0xFF) + "." +
                ((addr >>>= 8) & 0xFF) + "." +
                ((addr >>>= 8) & 0xFF));
    }

    public static String getPrivateIp(WifiManager wm) {
        int ip = wm.getConnectionInfo().getIpAddress();
        return intToIp(ip);
    }

    public static boolean verifyWifiConnection(WifiManager wm) {
        if (wm.isWifiEnabled()) {
            return wm.getConnectionInfo().getNetworkId() != -1;
        }
        return false;
    }

    public static String getGateway(WifiManager wm) {
        return intToIp(wm.getDhcpInfo().gateway);
    }

}

The function getGateway solves in an elegant way the problem of the LibreMesh Local-Address. The rest of the job was simply to change the address of the WebView to this one.

Using logcat to find bugs

The logic step then was to try the application and test if it worked okay, but when we did that the WebView that shows the Lime-App showed a white screen instead. Using the logcat inside the Android Studio we were able to easily find the error, showing the importance of using this type of debugging tools.

Using the logs it’s easy to see that there’s a TypeError when trying to get the property ‘getVoices’. The problem comes with the plugin ‘window.speechSynthesis’ that isn’t available for some browsers.

The Lime-App is the graphical interface that LibreMesh uses for the configuration of community networks. We found the .js that was calling the function:

let synth = window.speechSynthesis;
let voices = synth.getVoices();

export const speech = (text, lang) => {
let utterThis = new SpeechSynthesisUtterance(text);
utterThis.pitch = 0.9;
utterThis.rate = 1.2;
utterThis.voice = voices.filter(x => x.lang === lang)[0];
synth.cancel();
synth.speak(utterThis);
};

It can be seen that in the line 2 the variable voices is set to a synth.getVoices, but if synth is undefined, then that line will not succeed.
The solution was pretty simple, with a control structure we check if the speechSynthesis was available or not. So the fixed code is:

let synth = window.speechSynthesis;

export const speech = (text, lang) => {
if(synth != "undefined") {
let voices = synth.getVoices();
let utterThis = new SpeechSynthesisUtterance(text);
utterThis.pitch = 0.9;
utterThis.rate = 1.2;
utterThis.voice = voices.filter(x => x.lang === lang)[0];
synth.cancel();
synth.speak(utterThis);
}
};

I sent a pull request to the Lime-App repository fixing this problem and it’s currently waiting to be merged.

Next steps

With the functions of detecting and configuring a libremesh network, we plan to add some features to the app the next weeks:

  • A better graphical interface with the integrations of all the planned functions of the app.
  • Support other services in addition to Lime-App.
  • Add the app to the LibreMesh operating system, giving the posibility to the user to obtain the app directly from the router.

Video

Github project

One thought on “Building an app for network capability

Leave a Reply

Your email address will not be published. Required fields are marked *