Location Service in Android

Wherein you build an Android phone locator service.

Prerequisites:
All Java tutorials require Java and Gradle.

Introduction

In this tutorial, we will build a very simple Android application that will create a Vanadium server on the phone. When queried by an authorized client, the server will return the phone's physical location (latitude and longitude).

The tutorial will demonstrate some Android-specific aspects of the Vanadium implementation. It will also show that Vanadium allows clients to work across NAT networks transparently.

We will be building three software components in this tutorial:

Here's a screenshot of the tutorial program built and running on a Nexus 5 emulator.

Location service activity

Setting up the project

In this tutorial, we will use the Gradle build tool to build the project. There is no requirement that Vanadium Java projects use Gradle, but it's the easiest way to get started. See the installation instructions for details. The remainder of the tutorial will assume that you have the gradle program in your PATH.

If you are familiar with Android Studio, we encourage you to use it for this tutorial.

Build file

The first step to defining a Gradle project is to create a build.gradle file in the project root directory.

First, create a new project directory.

mkdir location
cd location

Now, create a build.gradle file:

cat <<EOF > build.gradle

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        // This introduces the Android plugin to make building Android
        // applications easier.
        classpath 'com.android.tools.build:gradle:1.3.1'

        // We are going to define a custom VDL service. The Vanadium
        // Gradle plugin makes that easier, so let's use that.
        classpath 'io.v:gradle-plugin:0.5'

        // Use the Android SDK manager, which will automatically download
        // the required Android SDK.
        classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.12.0'
    }
}

// Make our lives easier by automatically downloading the appropriate Android
// SDK.
apply plugin: 'android-sdk-manager'

// It's an Android application.
apply plugin: 'com.android.application'

// It's going to use VDL.
apply plugin: 'io.v.vdl'

repositories {
    mavenCentral()
}

android {
    compileSdkVersion 19
    buildToolsVersion "23.0.1"
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_7
        targetCompatibility JavaVersion.VERSION_1_7
    }
    packagingOptions {
        exclude 'META-INF/LICENSE.txt'
        exclude 'META-INF/NOTICE.txt'
    }
}

dependencies {
    compile 'io.v:vanadium:0.1'
    compile 'io.v:vanadium-android:0.1'
}

vdl {
    inputPaths += 'src/main/java'
}

EOF

Defining the Location Server

We're going to create a Vanadium server on the Android phone. The easiest way to do this is to define it in VDL.

mkdir -p src/main/java/io/v/location
cat <<EOF > src/main/java/io/v/location/location.vdl
package location

type LatLng struct {
        // The latitude of the phone, in degrees.
        Lat float64

        // The longitude of the phone, in degrees.
        Lng float64
}

type Location interface {
        // The one method that we will support. When called, returns the
        // physical location of the phone or an error if it could not be
        // determined.
        Get() (LatLng | error)
}

EOF

As you can see, we provide a single 'Get' method to get the location of the phone. Let's generate the VDL source just to make sure everything worked.

gradle vdl

You should see output like the following:

:prepareVdl
:extractVdl
:generateVdl
signature
time
vdltool
io/v/location
:removeVdlRoot
:vdl

BUILD SUCCESSFUL

The io/v/location line indicates that VDL tool has processed your input file. If you now look inside the generated-src directory, you'll find the following entries:

generated-src/vdl/io/v/location/LocationClientFactory.java
generated-src/vdl/io/v/location/LocationServer.java
generated-src/vdl/io/v/location/LocationClientImpl.java
generated-src/vdl/io/v/location/LatLng.java
generated-src/vdl/io/v/location/LocationClient.java
generated-src/vdl/io/v/location/LocationServerWrapper.java

Implementation

Now we must provide an implementation for the LocationServer. We'd like this server to be long-lived, so we're going to use an Android Service.

Create src/main/java/io/v/location/LocationService.java:

cat <<EOF > src/main/java/io/v/location/LocationServerImpl.java
package io.v.location;

import android.location.Criteria;
import android.location.Location;
import android.location.LocationManager;

import io.v.android.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.rpc.ServerCall;
import io.v.v23.verror.VException;

/**
 * This class implements the VDL interface we defined above.
 */
public class LocationServerImpl implements LocationServer {
   // We're going to use Android's LocationManager to get the phone's
   // physical location.
   private final LocationManager manager;

   LocationServerImpl(LocationManager manager) {
       this.manager = manager;
   }

   @Override
   public LatLng get(VContext context, ServerCall call) throws VException {
       Criteria criteria = new Criteria();
       criteria.setAccuracy(Criteria.NO_REQUIREMENT);
       String provider = manager.getBestProvider(criteria, true);
       if (provider == null || provider.isEmpty()) {
           throw new VException("Couldn't find any location providers on the device.");
       }
       Location location = manager.getLastKnownLocation(provider);
       if (location == null) {
           throw new VException("Got null location.");
       }
       return new LatLng(location.getLatitude(), location.getLongitude());
   }
}
EOF

Android service

Long-running tasks, like a Vanadium server, belong in Android services. In this section we implement an Android service that starts our LocationServer and mounts it into the Vanadium namespace. Please see the Android documentation for detailed information about Services.

Recall that Vanadium is secure. Given that this service is going to be responsible for mounting objects into the Vanadium namespace, we need to:

This service implementation will assume that this information is passed into the onCreate method.

Implementation

cat <<EOF > src/main/java/io/v/location/LocationService.java
package io.v.location;

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.location.LocationManager;
import android.os.IBinder;
import android.widget.Toast;

import com.google.common.collect.Lists;

import java.util.List;

import io.v.android.v23.V;
import io.v.v23.context.VContext;
import io.v.v23.rpc.ListenSpec;
import io.v.v23.rpc.Server;
import io.v.v23.security.BlessingPattern;
import io.v.v23.security.Blessings;
import io.v.v23.security.VCertificate;
import io.v.v23.security.VPrincipal;
import io.v.v23.security.VSecurity;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;

public class LocationService extends Service {
    public static final String BLESSINGS_KEY = "Blessings";
    private VContext baseContext;

    @Override
    public IBinder onBind(Intent intent) {
        return null;  // Binding not allowed
    }

    /**
     * This method decodes the passed-in blessings and calls
     * startLocationServer to actually start and mount the
     * Vanadium server.
     */
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Initialize Vanadium.
        baseContext = V.init(this);

        // Fetch the blessings from the intent. The activity that is starting
        // the service must populate this field.
        String blessingsVom = intent.getStringExtra(BLESSINGS_KEY);

        if (blessingsVom == null || blessingsVom.isEmpty()) {
            String msg = "Could not start LocationService: "
                + "null or empty encoded blessings.";
            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
            return START_REDELIVER_INTENT;
        }

        try {
            Blessings blessings = (Blessings) VomUtil.decodeFromString(
                blessingsVom, Blessings.class);
            if (blessings == null) {
                String msg = "Couldn't start LocationService: "
                    + "null blessings.";
                Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                return START_REDELIVER_INTENT;
            }

            // We have blessings, start the server!
            startLocationServer(blessings);
        } catch (VException e) {
            String msg = "Couldn't start LocationService: " + e.getMessage();
            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
        }
        return START_REDELIVER_INTENT;
    }

    /**
     * This method starts and mounts the Vanadium location server with the given
     * blessings.
     */
    public void startLocationServer(Blessings blessings) throws VException {
        // Principal represents our identity within the Vanadium system.
        VPrincipal principal = V.getPrincipal(baseContext);

        // Provide the given blessings when anybody connects to us.
        principal.blessingStore().setDefaultBlessings(blessings);

        // Also, provide these blessings when we connect to other services (for
        // example, when we talk to the mounttable).
        principal.blessingStore().set(blessings, new BlessingPattern("..."));

        // Trust these blessings and all the "parent" blessings.
        VSecurity.addToRoots(principal, blessings);

        // Our security environment is now set up. Let's find a home in the
        // namespace for our service.
        String mountPoint;
        String prefix = mountNameFromBlessings(blessings);

        if ("".equals(prefix)) {
            throw new VException("Could not determine mount point: "
                + "no username in blessings?");
        } else {
            mountPoint = "users/" + prefix + "/location";
            String msg = "Mounting server at " + mountPoint;
            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
        }

        // Now create the server and mount it.
        LocationServer locationServer = new LocationServerImpl(
            (LocationManager) getSystemService(Context.LOCATION_SERVICE));

        // Use Vanadium's production proxy server for NAT traversal. None of
        // your data is visible to the proxy server because it's all encrypted.
        ListenSpec spec = V.getListenSpec(baseContext).withProxy("proxy");

        // Finally, the magic moment!
        Server server = V.getServer(
            V.withNewServer(V.withListenSpec(baseContext, spec),
                mountPoint, locationServer, null));

        Toast.makeText(this, "Success!", Toast.LENGTH_SHORT).show();
    }

    /**
     * This method finds the last certificate in our blessing's certificate
     * chains whose extension contains an '@'. We will assume that extension to
     * represent our username.
     */
    private static String mountNameFromBlessings(Blessings blessings) {
        for (List<VCertificate> chain : blessings.getCertificateChains()) {
            for (VCertificate certificate : Lists.reverse(chain)) {
                if (certificate.getExtension().contains("@")) {
                    return certificate.getExtension();
                }
            }
        }
        return "";
    }
}
EOF

Android activity

We have defined a service, now we need an activity to run it. This activity has only two UI elements: a button to choose a Vanadium blessing and a button to start the location service.

Security and the Account Manager

Now would be a good time to talk some more about security. Since this tutorial involves talking to the Vanadium root mounttable, we're going to need a blessing issued by the Vanadium identity service. The details of how a trusted channel is established between two authenticated endpoints is beyond the scope of this tutorial. To make a long story short: we're going to delegate all of this to a special Android application called the Account Manager.

You should download and install the account manager using the following commands:

wget https://v.io/account_manager-release.apk
$HOME/.android-sdk/platform-tools/adb install -r account_manager-release.apk

Implementation

Assuming that you've installed the account manager, you're ready to implement the location activity. Typically, Android applications will use declarative XML layouts, but to keep this tutorial as short as possible, we're going to create and lay out the UI components manually in Java.

cat <<EOF > src/main/java/io/v/location/LocationActivity.java

package io.v.location;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Toast;

import io.v.android.libs.security.BlessingsManager;
import io.v.android.v23.V;
import io.v.android.v23.services.blessing.BlessingCreationException;
import io.v.android.v23.services.blessing.BlessingService;
import io.v.v23.context.VContext;
import io.v.v23.security.Blessings;
import io.v.v23.verror.VException;
import io.v.v23.vom.VomUtil;

public class LocationActivity extends Activity {
    private static final int BLESSING_REQUEST = 1;

    private VContext mBaseContext;
    private Button chooseBlessingsButton;
    private Button startServiceButton;
    private Blessings blessings;

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

        // Initialize Vanadium.
        mBaseContext = V.init(this);

        // Layout our two buttons vertically.
        LinearLayout layout = new LinearLayout(getApplicationContext());
        layout.setOrientation(LinearLayout.VERTICAL);

        // Initially the blessings will be null. Give the user a button to pick
        // the blessings to use.
        chooseBlessingsButton = new Button(getApplicationContext());
        chooseBlessingsButton.setText("Choose blessings...");
        chooseBlessingsButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                fetchBlessings(false);
            }
        });
        layout.addView(chooseBlessingsButton);

        // Once they've picked blessings, this button will be enabled and will
        // allow the user to start the location service.
        startServiceButton = new Button(getApplicationContext());
        startServiceButton.setText("Start listening");
        startServiceButton.setEnabled(false);
        startServiceButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startLocationService(blessings);
            }
        });
        layout.addView(startServiceButton);

        setContentView(layout);
    }

    /**
     * This method is called when the user clicks the "choose blessings" button.
     * It fires off an intent which will be handled by the Account Manager.
     */
    private void fetchBlessings(boolean startService) {
        Intent intent = BlessingService.newBlessingIntent(this);
        startActivityForResult(intent, BLESSING_REQUEST);
    }

    /**
     * This method will be called once the user has finished selecting their
     * blessings from the Account Manager.
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        switch (requestCode) {
            case BLESSING_REQUEST:
                try {
                    // The Account Manager will pass us the blessings to use as
                    // an array of bytes. Use VomUtil to decode them...
                    byte[] blessingsVom =
                        BlessingService.extractBlessingReply(resultCode, data);
                    blessings = (Blessings) VomUtil.decode(blessingsVom, Blessings.class);
                    BlessingsManager.addBlessings(this, blessings);
                    Toast.makeText(this, "Success, ready to listen!",
                        Toast.LENGTH_SHORT).show();

                    // Enable the "start service" button.
                    startServiceButton.setEnabled(true);
                } catch (BlessingCreationException e) {
                    String msg = "Couldn't create blessing: " + e.getMessage();
                    Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                } catch (VException e) {
                    String msg = "Couldn't store blessing: " + e.getMessage();
                    Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
                }
                return;
        }
    }

    /**
     * Finally, this is the start service button handler.
     */
    private void startLocationService(Blessings blessings) {
        try {
            // Recall that the location service expects a Blessings object
            // encoded as a VOM string.
            String blessingsVom = VomUtil.encodeToString(blessings, Blessings.class);
            Intent intent = new Intent(this, LocationService.class);
            intent.putExtra(LocationService.BLESSINGS_KEY, blessingsVom);
            stopService(intent);
            startService(intent);
        } catch (VException e) {
            String msg = String.format(
                    "Couldn't encode blessings %s: %s", blessings, e.getMessage());
            Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
        }
    }
}
EOF

Android manifest file

Android applications require a manifest file. In our case, it's pretty straightforward:

cat <<EOF > src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.v.location"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="21" />

    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <application>
        <activity
            android:name=".LocationActivity"
            android:label="Location Service" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <service
          android:name=".LocationService"
          android:exported="false"
          android:label="Location Service"
          android:process=":LocationService">
        </service>
    </application>
</manifest>
EOF

We'll create a resource file to hold some values. The Android Gradle plugin will fail if no resources are defined.

mkdir -p src/main/res/values
cat <<EOF > src/main/res/values/strings.xml
<resources>
    <string name="app_name">Location</string>
</resources>
EOF

Deploying and running the application

Finally, you're ready to build and deploy your Android application. To do this:

gradle assembleRelease

This will produce file named build/outputs/location-release-unsigned.apk. You can install this on your phone:

$HOME/.android-sdk/platform-tools/adb \
    install -r build/outputs/location-release-unsigned.apk

You will now find your application in your phone's application list. When you run it, you will need to pick some blessings. Choose your email address from the account list.

Calling the server

The server is now running on your phone and listening for connections. Now we can connect to it from another computer. We'll use the vrpc command in the Vanadium distribution. Once you've installed that, we need to get ourselves some blessings to talk to the phone.

The Vanadium server that we placed into the Vanadium namespace uses a default authorizer. This means that it will trust an incoming connection if the client provides blessings that share a common name with the server.

$HOME/v23_release/bin/principal \
    --v23.credentials=/tmp/creds seekblessings

# This command will open a browser. In it, you should sign in to Google using
# the same email address that you used on your phone.

Now let's make the call:

$HOME/v23_release/bin/vrpc \
    --v23.credentials=/tmp/creds \
    call /users/you@gmail.com/android/io.v.location/location

Try making some changes to the phone's network configuration. For example, disconnect from the Wi-Fi network so that your phone is using a mobile data connection. Your location requests should still be successful.

Summary

Congratulations! You have successfully built and run the location service application on Android.

You have:

There are a few things to note: