Skip Navigation
Trulia Logo

Trulia Blog

Bend StreetView on Android to your Will

With the last major release of our Android app we added in Street View capability on property detail pages. The response we got from users was extremely positive but we couldn’t help but notice a few issues that really degraded a user’s experience.

First, we assumed (incorrectly) that the built in Street View app would be available on all Android phones. And how did that work out? The stack traces in the Market crash reports told the whole story: some people didn’t have the app installed.

Secondly, we were unable to determine if a location actually had street view information available from Google. While the newest JavaScript Maps API includes this capability, the Android API does not. Therefore it was totally possible for a user to get a black screen in the Street View app and have their expectations squished. Users were annoyed with this issue and many times blamed Trulia for the problem.

Thirdly, even if the user had Street View installed and the street view for the property actually existed, we were forced to use the default yaw (angle) when firing our Intent. This lead to displays that did not show the property reliably and again annoyed our users.

So the question became how to overcome the limitations of the Android Maps API and squash all three of these issues. It took some time but we did find a viable way to make the user experience all that it can be.

So how did we do it? By utilizing the ability to bind JavaScript with Android, we created a hidden WebView on the property detail page that could interface with our Java and also the JavaScript version of the Google Maps API (3.0). We also do a simple comparison between the Street View Intent string (com.google.android.street) and the app packages installed on the user’s phone.

I spent a lot of time online looking for information on this issue and cobbled together my solution using information from a couple of postings as well as my own trial and error.

The first step was to just get the Package checking working with the following logic:

  1. Get packages from PackageManager
  2. Loop through all packages looking for the string “com.google.android.street”
  3. If not found return false, otherwise return true and load the button and fire off the JavaScript
public int hasInstalledPackage(String packageName) {
  List packs = mContext.getPackageManager().getInstalledPackages(0);
  for (int i = 0; i > packs.size(); i++) {
   PackageInfo p = packs.get(i);
     if (packageName != null && !"".equals(packageName)
                                           && packageName.equals(p.packageName))
       return 1;
     }
   return 0;
}

Then you can call it like this:

int hasStreetView = hasInstalledPackage("com.google.android.street");
if(hasStreetView) {
  // we can proceed with the rest of the street view code
}
else {
  // street view is not installed so hide the button or link in your view
}

Build the html page
We need to create the hidden JS page we will use to contact the Google Maps JS API. In your android application create an “assets” folder on the same file level as the “src” and “res” folders. Then create an html file in that folder. I call my file “streetview_js_check.html.”

In the “head” of your html file make sure to link to the Google JS API:

<script src="http://maps.googleapis.com/maps/api/js?sensor=false" type="text/javascript"><!--mce:0--></script>

Then add a script tag and place the following JavaScript in it:

var position = null;
var panorama = null;
var point = null;
var map = null;
var panoOptions = null;
var heading = null;

function startup(lat,lng) {
  try {
    newStreetView(lat,lng);
  } catch (ee) {
    window.streetView.putError(ee);
  }
}

function getHeading() {
  try {
    window.streetView.putDebug("get_heading");
    return heading;
  } catch (ee) {
    window.streetView.putError(ee);
  }
}

function newStreetView(lat,lng) {
  window.streetView.putDebug("newStreetView");

  point = new google.maps.LatLng(lat, lng);
  panorama = new google.maps.StreetViewPanorama(document.getElementById("pano"));

  var service = new google.maps.StreetViewService();
  service.getPanoramaByLocation(point, 50, showPanoData);

  var myOptions = {
    zoom: 14,
    center: point,
    mapTypeId: google.maps.MapTypeId.ROADMAP
  };

  var map = new google.maps.Map(document.getElementById("map_canvas"), myOptions);
  var marker = new google.maps.Marker({
    map: map,
    position:point
  });

}

function showPanoData(panoData, status) {
  window.streetView.putDebug("showPanoData");

  if (status != google.maps.StreetViewStatus.OK) {
    window.streetView.setLatLngStr("BAD_STATUS");
  }
  else {
    heading = angle = computeAngle(point, panoData.location.latLng);
    window.streetView.setLatLngStr(heading);

    /* UNCOMMENT IF YOU WANT TO SEE THE ACTUAL STREET VIEW
    document.getElementById("pano").style.display = "block";

    var panoOptions = {
      position: point,
      pov: {
        heading: angle,
        pitch: 10,
        zoom: 1
      },
      visible:true
    };

    panorama.setOptions(panoOptions);
    /* END OF COMMENT STREET VIEW OUT */

  }
}

function computeAngle(endLatLng, startLatLng) {
  var DEGREE_PER_RADIAN = 57.2957795;
  var RADIAN_PER_DEGREE = 0.017453;

  var dlat = endLatLng.lat() - startLatLng.lat();
  var dlng = endLatLng.lng() - startLatLng.lng();
  var yaw;

  try {
    yaw = (Math.atan2(dlng * Math.cos(endLatLng.lat() * RADIAN_PER_DEGREE), dlat))
                      * DEGREE_PER_RADIAN;
    return wrapAngle(yaw);
  } catch (ee) {
    alert(ee);
  }

}

function wrapAngle(angle) {
  if (angle >= 360) {
    angle -= 360;
  } else if (angle < 0) {
    angle += 360;
  }
  return angle;
}

In your body you can use this html to hide or view the actual streetview:

<h3>Streetview Lookup</h3>

You will need to change the display and width/height values as well.

Now onto the main chunk of code that will make the magic happen.

Custom Web Client – used to pass in the lat/lng of a property and fire off the initial JavaScript Maps lookup

import android.webkit.WebView;
import android.webkit.WebViewClient;

public class StreetViewWebViewClient extends WebViewClient {

  private WebView webView = null;
  private String latLng;

  public StreetViewWebViewClient(WebView view, String latLngInput) {
    webView = view;
    latLng = latLngInput;
  }

  public void onPageFinished(WebView view, String url) {
    webView.loadUrl("javascript:startup(" + latLng + ")");
  }
}

Custom Handler – used to call JavaScript functions and also update our Android main thread handler

import android.os.Bundle;
import android.os.Handler;
import android.os.Message;

public class StreetViewHandler {

  public final static String STREETVIEW_HEADING = "heading";
  public final static String STREETVIEW_BAD_STATUS = "BAD_STATUS";

  public final static int ACTION_STREET_VIEW_RESULT = 2001;

  private Handler viewHandler = null;

  public StreetViewHandler(Handler h) {
    viewHandler = h;
  }

  // log debug message
  public void putDebug(String str) {
    Log.d("StreetViewTest",str);
  }

  // special function to log an error message
  public void putError(String str) {
    Log.e("StreetViewTest",str);
  }

  public void setLatLngStr(String str) {

    if (viewHandler != null) {
      Message msg = new Message();
      msg.what = ACTION_STREET_VIEW_RESULT;

      Bundle b = new Bundle();
      b.putString(STREETVIEW_HEADING, str);
      msg.setData(b);
      viewHandler.sendMessage(msg);
    } else {
      Log.w("StreetViewTest","viewHandler is null");
    }
  }
}

JS Controller – deals with setting up the hidden WebView, passing our Custom Web Client, and binding our Android handler to the JavaScript in the HTML file

import android.os.Handler;
import android.webkit.WebSettings;
import android.webkit.WebView;

/*
* Help provided from:
*
* Google Maps JS Api StreetView
* http://www.jaycodesign.co.nz/js/
* using-google-maps-to-show-a-streetview-of-a-house-based-on-an-address/
*
* Android / Javascript Binding
* http://www.ibm.com/developerworks/xml/library/x-andbene2/index.html
*
*/

public class StreetViewJSController {

  private WebView browser = null;
  private String latLngInput;
  private Handler viewHandler;

  public StreetViewJSController(WebView view, Handler handle) {
    browser = view;
    viewHandler = handle;
  }

  public void setLatLngInput(String str) {
    Log.d("StreetViewTest","latLngInput = " + str);
    latLngInput = str;
  }

  public void fetchHeading(String file) {

    // create a new custom client and pass it into our WebView
    StreetViewWebViewClient webClient =
      new StreetViewWebViewClient(browser, latLngInput);
    browser.setWebViewClient(webClient);

    // need to grab the setting and enable javascript in the WebView
    WebSettings settings = browser.getSettings();
    settings.setJavaScriptEnabled(true);

    // should also clear the cache just for the heck of it
    browser.clearCache(true);

    // if you want to use alert() in your js you need to set this value;
    // otherwise just keep it commented out
    // browser.setWebChromeClient(new WebChromeClient());

    // now we need to bind our Android handler to a javascript keyword that we
    // can then access in our html/js via window.streetView
    browser.addJavascriptInterface(new StreetViewHandler(viewHandler),"streetView");

    // the last thing to do is load our html file into the WebView
    browser.loadUrl(file);

    // debug message to know that it's done loading
    Log.d("StreetViewTest","file loaded");

  }
}

It is not the most elegant solution, but it is the best one I have found to this problem. There are still a couple of issues to be dealt with:

  • Minimize the calls to the JS Map API. You could do this by creating a shared preference value. Or storing the info in a DB. At the very minimum you need to take into account orientation switching and keyboard opening (set a boolean and pass it in the savedInstanceState bundle).
  • Deal with any rate limiting Google enforces for your particular site. You might have to get a new API key for this specific case. Or maybe your site (like Trulia’s) uses maps religiously and therefore you’ll have to work with Google to increase your rate limit. The one thing you don’t want to have happen is to surpass that rate limit and start getting error messages in your calls. The user will hate that and you will hear about it.

For more reading I suggest you check out the two postings I referenced above: