Life, the Universe and Everything
Back to mainpage

Writing Hangout Apps 2
Connecting to Google APIs


After learning the basics of Hangout Apps, here's a more advanced topic. We're going to learn how to use additional OAuth-Scopes and the Google APIs Javascript library to get more information into your Hangout apps.

Introduction

I'm going to assume that you have read and understood my previous article because we're going to start developing from there.

The plan for this article is to develop an app which uses the Google APIs JavaScript Client to access the private Google Latitude™ data via the Latitude API and let people share their current city-based location with the other participants if they want to. The locations will be displayed using the Google Maps™ API.

You can find the full sources for the simple app we are going to build on GitHub and on Google Code.

1 - Google API Console settings

In addition to the settings explained last time we will need to do some more things in the Google APIs console to access the other APIs we need.

In "Services" we need to enable "Google Maps API v3" to display the map and "Latitude API" to access the users location.



In "Hangouts" we need to enable additional OAuth 2.0 scopes and add "https://www.googleapis.com/auth/latitude.current.city" which allows us to request the current location of the user with city granularity. Check the Latitude API documention to see what other scopes you could request.



2 - Gadget XML

In the XML file we are going to load two additional scripts to access the Google Maps API and the Google APIs JavaScript Client. We will also define some HTML elements which will include the information and be used for interaction. The new parts as compared to last time are highlighted and described below.


Note: you will have to replace <YOUR_PATH> with the location where you are planning to upload the files.


<?xml version="1.0" encoding="UTF-8" ?>
<Module>
<ModulePrefs title="Hangout Demo">
<Require feature="rpc"/>
<Require feature="views"/>
</ModulePrefs>
<Content type="html">
<![CDATA[
<script src="//talkgadget.google.com/hangouts/_/api/hangout.js?v=1.1"></script>
<script src="//www.google.com/jsapi"></script>This includes the Google Maps API
<link rel="stylesheet" href="<YOUR_PATH>/css/demo.css" />
<h1>Hangout Location Demo</h1>
<div id="map"></div>This will include the rendered map
<div id="my_location">
<p id="current_location" class="small"></p>This will display your current location
<button id="share" disabled="disabled">Share location</button>Clicking will share your location with the others
<button id="unshare" disabled="disabled">Remove location</button>Clicking will hide your location
</div>
<script src="<YOUR_PATH>/scripts/demo.js"></script>
<script src="//apis.google.com/js/client.js?onload=onClientReady"></script>This loads the Google APIs JS Client.
When ready this will call the
onClientReady function in our demo.js script.
]]>
</Content>
</Module>


3 - Authenticating and preparing the APIs

We're going to start with a rather empty script, defining the onClientReady function, initializing our App with some variable we will need later and waiting for the Hangouts API to be ready before doing anything else.

(function (window) {
"use strict";
var locationApp;
function LocationApp() {
this.map = null;map object for adding locations
this.latlng = {};current location of this participants
this.locations = {};shared locations of other participants
this.api_key = "<YOUR API KEY>";insert your key from the API console here
gapi.hangout.onApiReady.add(this.onApiReady.bind(this));waiting for API to be ready
}
LocationApp.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
// ...
}
};
window.onClientReady = function () {
locationApp = new LocationApp();start our app
};
}(window));


Once the Hangouts API is ready the first thing we are going to do is loading the Google Maps API and preparing the (empty) map.

LocationApp.prototype.onApiReady = function (event) {
if (event.isApiReady === true) {
google.load(this loads the Google Maps Javascript API v3
"maps", "3", {
other_params: "key=" + this.api_key + "&sensor=false",
callback: this.mapsReady.bind(this)this function will be called when the API is ready
});
}
};
LocationApp.prototype.mapsReady = function () {
this.prepareMap();
// we'll do some more stuff here soon
};
LocationApp.prototype.prepareMap = function () {
var latlng, myOptions;
latlng = new google.maps.LatLng(0, 0);
myOptions = {some options as to how the map is displayed
zoom: 0,see the Maps API documentation for details
center: latlng,
disableDefaultUI: true,
zoomControl: true,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
this.map = new google.maps.Map(save a reference to the map for later
document.getElementById("map"), my Optionsrender the map inside our div
);
};


After the map is prepared we're going to authenticate the Google APIs JS Client to access Latitude. The JS Client works together nicely with the Hangout, using the already existing OAuth tokens without the need for another explicit OAuth workflow for the user. That's why we already added the necessary OAuth scope in the API console.

LocationApp.prototype.mapsReady = function () {
var scopes;
this.prepareMap();
scopes = [
"https://www.googleapis.com/auth/plus.me",OAuth scopes necessary for the Hangouts API
"https://www.googleapis.com/auth/hangout.av",.
"https://www.googleapis.com/auth/hangout.participants",.
"https://www.googleapis.com/auth/latitude.current.city"OAuth scope for accessing Latitude
];
gapi.client.setApiKey(null);the client will automatically use the API Key
associated with the Hangout App project
gapi.auth.authorize({this will authenticate the client using the
client_id: null,ClientID associated with the Hangout App project
scope: scopes,to access the data, the user already has
immediate: truegranted permission to use, and will call
}, this.handleAuthResult.bind(this));our function once the OAuth workflow
};has finished in the background
LocationApp.prototype.handleAuthResult = function (authResult) {
if (authResult) {
// authentication worked, we can access latitude now
} else {
document.getElementById("current_location").innerHTML =
"Can't determine location: not authorized";
}
};

Note: after authentication you can actually use gapi.auth.getToken() to get the OAuth Access Token which you can use in any API calls, even for those that are not available in the Google APIs JS Client, provided you have requested the necessary scopes.

4 - Accessing the Data

Once authorized we can use the Latitude API to fetch the current location for the user.

LocationApp.prototype.handleAuthResult = function (authResult) {
if (authResult) {
gapi.client.load(This loads the necessary information to
'latitude', 'v1',access the Latitude API and calls our
this.loadLocation.bind(this)function once ready.
);
} else {
document.getElementById("current_location").innerHTML =
"Can't determine location: not authorized";
}
};
LocationApp.prototype.loadLocation = function () {
var request;
request = gapi.client.latitude.currentLocation.get(Here we create a request to get
{"granularity": "city"}the user's current city location.
);See the Latitude API documentation for details.
request.execute(function (loc) {Executing the request will return a JSON object
var wp;
if (loc.error) {with either an error
document.getElementById("current_location").innerHTML =
"Can't determine location: " + loc.error.message;
} else {
document.getElementById("current_location").innerHTML =or with the latest geographic coordinate
"Current location: " + loc.latitude + " " + loc.longitude;with latitude and longitude.
this.latlng.lat = loc.latitude;Saving the coordiantes
this.latlng.lng = loc.longitude;for later use.
wp = new google.maps.LatLng(this.latlng.lat, this.latlng.lng);Here we construct a Google maps marker
this.locations[gapi.hangout.getParticipantId()] =to display the current location
new google.maps.Marker({position: wp, map: this.map});to the user.
}
}.bind(this));
};


Once we have the location we want to allow the user to share (and unshare) it with the participants of the hangout. For this we need to add some functions that interact with the shared state.

LocationApp.prototype.loadLocation = function () {
var request;
request = gapi.client.latitude.currentLocation.get(
{"granularity": "city"}
);
request.execute(function (loc) {
var wp, button;
if (loc.error) {
document.getElementById("current_location").innerHTML =
"Can't determine location: " + loc.error.message;
} else {
document.getElementById("current_location").innerHTML =
"Current location: " + loc.latitude + " " + loc.longitude;
this.latlng.lat = loc.latitude;
this.latlng.lng = loc.longitude;
wp = new google.maps.LatLng(this.latlng.lat, this.latlng.lng);
this.locations[gapi.hangout.getParticipantId()] =
new google.maps.Marker({position: wp, map: this.map});
button = document.getElementById("share");Enable "Share"-button since we
button.removeAttribute("disabled");have a location to share now
button.onclick = this.shareLocation.bind(this);and bind click event handlers
button = document.getElementById("unshare");to the buttons.
button.onclick = this.unshareLocation.bind(this);
}
}.bind(this));
};
LocationApp.prototype.shareLocation = function () {
var button;
button = document.getElementById("share");Just some DOM manipulations
button.setAttribute("disabled", "disabled");to update the buttons.
button = document.getElementById("unshare");
button.removeAttribute("disabled");
gapi.hangout.data.setValue(
gapi.hangout.getParticipantId(),Participant ID is used as unique key
JSON.stringify(this.latlng)Shared state only accepts string values
);
};
LocationApp.prototype.unshareLocation = function () {
var button;
button = document.getElementById("unshare");Just some DOM manipulations
button.setAttribute("disabled", "disabled");to update the buttons.
button = document.getElementById("share");
button.removeAttribute("disabled");
gapi.hangout.data.clearValue(Remove the shared value
gapi.hangout.getParticipantId()associated with the current user.
);
};


And of course since we are sharing locations we also need functions to handle shared locations by other participants. Because we need the map to display those locations we will initialize those functions after preparing the maps.

LocationApp.prototype.mapsReady = function () {
(...)
gapi.hangout.data.onStateChanged.add(You should already know this
this.onStateChanged.bind(this)event handler from last time.
);
this.showAllLocations();When we first start the app, we also
want to display already shared locations.
};
LocationApp.prototype.onStateChanged = function (event) {
var i, l, id, latlng;
l = event.addedKeys.length;addedKeys includes all newly shared locations
for (i = 0; i < l; i++) {
id = event.addedKeys[i].key;The key is the unique participant ID
of the other participant
latlng = JSON.parse(event.addedKeys[i].value);Also see above in the shareLocation function
this.addLocation(id, latlng.lat, latlng.lng);
}
l = event.removedKeys.length;removedKeys includes the keys, i.e. IDs of
for (i = 0; i < l; i++) {the participants who removed their location
id = event.removedKeys[i];
this.removeLocation(id);
}
};
LocationApp.prototype.addLocation = function (id, lat, lng) {
var latlng, info, image_url, image, participant;
if (id !== gapi.hangout.getParticipantId()) {prevent the own location from being added again
if (!this.locations[id]) {only add really new locations
participant = gapi.hangout.getParticipantById(id);get profile information about the participant
if (participant && participant.person) {
info = participant.person.displayName;
image_url = participant.person.image.url;
image = new google.maps.MarkerImage(create a map marker with the profile image
image_url,and add it to the map
new google.maps.Size(32, 32),
new google.maps.Point(0, 0),
new google.maps.Point(16, 16),
new google.maps.Size(32, 32)
);
latlng = new google.maps.LatLng(lat, lng);
this.locations[id] = new google.maps.Marker({
position: latlng,
map: this.map,
title: info,
icon: image
});
}
}
}
};
LocationApp.prototype.removeLocation = function (id) {
if (id !== gapi.hangout.getParticipantId()) {prevent the own location from being removed
if (this.locations[id]) {
this.locations[id].setMap(null);remove marker from map
delete this.locations[id];delete location information
}
}
};


When we first launch the app we want to display the locations that have already been shared before we joined.

LocationApp.prototype.mapsReady = function () {
(...)
gapi.hangout.data.onStateChanged.add(
this.onStateChanged.bind(this)
);
this.showAllLocations();
};
LocationApp.prototype.showAllLocations = function () {
var state, id, latlng;
state = gapi.hangout.data.getState();Get the currently shared locations
for (id in state) {
if (state.hasOwnProperty(id)) {
latlng = JSON.parse(state[id]);
this.addLocation(id, latlng.lat, latlng.lng);Add them to the map
}
}
};


Since location information is rather sensitive information we want to make sure that the location is removed when participants leave the hangout or disable the app. For this we can add two more event handlers to events provided by the Hangouts API.

LocationApp.prototype.mapsReady = function () {
(...)
gapi.hangout.onParticipantsDisabled.add(Called when someone disables the app
this.removeParticipants.bind(this)
);
gapi.hangout.onParticipantsRemoved.add(Called when someone leaves the Hangout
this.removeParticipants.bind(this)
);
};
LocationApp.prototype.removeParticipants = function (event) {
var i, l, remove, participants;
participants =
event.disabledParticipants ||Handling the two different events accordingly
event.removedParticipants || [];
l = participants.length;
remove = [];Create an array of all participant IDs
for (i = 0; i < l; i++) { i.e. keys to be removed
remove.push(participants[i].id);
}
gapi.hangout.data.submitDelta({}, remove);This will remove the locations from the shared state.
The actual removal from the map will be done
afterwards via the onStateChanged event above.
};


Conclusion

Adding information from other Google services offers a whole lot possibilities for Hangout Apps and I'm looking forward to see what ideas you come up with.


As I already mentioned above you can find the full sources for this app on GitHub and on Google Code.

Here's approximately how the app will look like in action:



When looking at the source you might noticed an additional zoom function. This one just makes sure that all locations are always visible on the map, adjusting the zoom and map location as necessary whenever locations are added or removed.

References

Hangouts API Documentation: developers.google.com/+/hangouts/
Google APIs Javascript Client: code.google.com/p/google-api-javascript-client/
Maps API Documentation: developers.google.com/maps/documentation/javascript/
Latitude API Documentation: developers.google.com/latitude/
Several code samples by myself: github.com/Scarygami/gplus-experiments or code.google.com/p/gplus-experiments/
Directory of hangout apps: hangoutapps.com



Google+, Google Maps and Google Latitude are trademarks of Google Inc. Use of these trademarks is subject to Google Permissions.
This site is not affiliated with, sponsored by, or endorsed by Google Inc.


Back to mainpage

This work is licensed under a Creative Commons Attribution 3.0 Unported License.