Introduction
I've got a small collection of wearables on my desk. The Tizen powered Galaxy Gearm the Android Wear powered Galaxy Live, Google Glass, a heart rate monitor, and there's even a 2004 Microsoft S.P.O.T. Watch among the items. With the exception of the Microsoft S.P.O.T. watch I can interact with all of these devices via my own programs some how. I thought it would be fun to do some type of "Hello World" program for all of them. A speedometer seemed to be simple yet function enough; get the speed and display it to the user as text. Despite the functionality being simple making such a program does touch on a number of different aspects of the wearable that are more useful than trivial.
In this article I'm going to make the speedometer with the Tizen powered Galaxy Gear. I've got the orignal Galaxy Gear that was released in late 2013. At the time of this writing a secong generation of gears has been released (there are three different models in the second generation) and the third generation Galaxy Gear S has been announced but not released. The differences between the first and second generation units (in terms of function) is small. Most of them could be viewed as only varying on whether or not they have a camera, whether or not a heart rate monitor, and whether or not there is an infared emitter. The significant difference is that one of the models has a screen of a different of a different pixel dimension than the other units.
To get started with this article you will want to have done the following:
- Setup the Tizen SDK for Wearables as described in Samsung's article
- Have setup an Android development environment
- Have a Galaxy Gear series watch in your possession
It is possible to do development without owning a Galaxy Gear by using the emulator but I don't If you have the original Galaxy Gear you'll want to have applied the firmware update that changes the watch's operating system from Android to Tizen.
What is Tizen
Tizen is an open source mobile operating system made by Samsung, Intel, and a few others. It is self described as "The OS of Everything" (tizen.org) with planned support for televisions, phones, wearables, and more. Right now the the only implementations running diretly on Hardware are for the Galaxy Gear (watches) and with another device (Galaxy Gear S) coming soon. The first Tizen phone is expected within the next year. Tizen supports development of both native programs and programs written in HTML. The HTML based projects are packaged accoding to W3C Packaged Web Apps (Widgets) - Packaging and XML Configuration. I'll be making use of the HTML development environment for this first program.
Where's the GPS
If you take a look in the Tizen documentation there is mention of GPS. The Tizen operating system does support and already has APIs defined for GPS. But the presence of support of a feature in the operating system doesn't imply the presence of the hardware in something that makes use of that operating system (ex:Windows supports a computer having GPS, nut not all computers have GPS receivers). The watch that I am using does not have GPS. For this program to work it is necessary to send a request to the phone to turn on it's GPS and relay the information back to the watch.
In my research on how to do this I came across the Samsung Remote Sensor SDK. Its for getting sensor data from one device and sending it to another. It turns out this isn't what I thought it was. This feature is for retrieving sensor data from the watch and getting it back to the phone. I need for data to flow in the other direction. After a bit more research it became apparent that what I needed was to make my own Android application that would retrieve this information for me. I don't need for the android application to have any UI so it only needs to run as a service. Looking a bit deeper I found that what I needed was the Samsung Accessory SDK.
A Tizen based watch that does support GPS is coming but hasn't been released as of the time that I am writing this. I'll discuss how it might be addressed at the end of this article.
Samsung Accessory SDK
The Samsung Accessory SDK is a solution Samsung provides for devices to interact with each other. Devices can take the role of a provider or a consumer and can be connected to multiple instances of applications that have the complementary relationship. The accessory SDK takes care of the details of the discovery of the applications that act as providers and services on the devices and abstract away the details of communication between them. Communication can occur over a number of transports (Wi-Fi, Bluetooth, USB, and some others). A consumer can connect to many providers or a provider can be attached to many consumers.
The devices that are involved in these interactions are generically referred to as Accessory Peers. The peers will expose one or more software instances that either provide information and functionality or consume it. Regardless of whether it is a provider or a consumer the software that is providing or consuming is called an Accessory Peer Agent (represented by the class SAAgent
in the Java code on the Android side). The peer agents are connected through a Service Connection (in the Java code this is defined by the SASocket
class). Within a service connection there can exist many Service Channels. Service channels are logical units within a connection. Each service channel can be assigned different QoS (Quality of Service) parameters for the data that is being transmitted, so as whether or not that connection is reliable (an undelivered message will be redelivered or not) or not (dropped packets will just be lost and not retransmitted) whether or not fast connection is needed, and optionally the amount of time needed for establishing a connection.
The connection information for an application must be registered with a Service Profile. The service profile is defined in XML and the inclusion of the XML as a resource in your project is all that you need to do to ensure that the registration occurs. Within this profile is information that uniquely identifies your Peer Agent and the QoS parameters for the communciations channels.
Creating the Visual Interface
The visual interface for this project is going to completely run on the watch. The phone will present no interface beyond a notification that the GPS is active. In the Tizen for Wearables IDE create a new jQuery project. The project that the IDE builds will toggle between two words when the screen is touched in a text element called content_text
. Erase the phrase that is within this element and replace it with something else that indicates the software is waiting for a GPS connection. When the application starts this will be the indication that GPS information is not yet being received. The code for the UI will look as follows.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="A single-page template generated by Tizen Wearable Web IDE"/>
<title>Speedometer</title>
<script type="text/javascript" src="js/jquery-1.9.1.js"></script>
<script type="text/javascript" src="js/main.js"></script>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class=contents>
<div style='margin:auto;'>
<span class=content_text id=textbox>waiting on gps</span>
</div>
</div>
</body>
</html>
The location notification icon is the only visual indicator on the phone that our service is running
Registering the Consumer
Registration within the Tizen project occurs automatically if a XML file is referenced that contains the profile definition. The two parts to doing this are creating the file and letting the system know where that file is located. In your Tizen project from the project root make a folder called res
. Within res
make a subfolder named xml
. Inside of the xml
folder create an XML file. I've called mine sapservices.xml
. You can name your's differently if you like but remember to substitute the name that you choose where I specify sapservices.xml
.
The document type definition can optionally be included at the top of the profile definition. I encourage doing this since it can help identify errors in the definition.
<!DOCTYPE resources [
<!ELEMENT resources (application)>
<!ELEMENT application (serviceProfile)+>
<!ATTLIST application name CDATA #REQUIRED>
<!ELEMENT serviceProfile (supportedTransports, serviceChannel+) >
<!ATTLIST application xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile serviceImpl CDATA #REQUIRED>
<!ATTLIST serviceProfile role (PROVIDER | CONSUMER | provider | consumer) #REQUIRED>
<!ATTLIST serviceProfile name CDATA #REQUIRED>
<!ATTLIST serviceProfile id CDATA #REQUIRED>
<!ATTLIST serviceProfile version CDATA #REQUIRED>
<!ATTLIST serviceProfile serviceLimit
(ANY | ONE_ACCESSORY | ONE_PEERAGENT | any | one_peeragent | one_accessory) #IMPLIED>
<!ATTLIST serviceProfile serviceTimeout CDATA #IMPLIED>
<!ELEMENT supportedTransports (transport)+>
<!ATTLIST supportedTransports xmlns:android CDATA #IMPLIED>
<!ELEMENT transport EMPTY>
<!ATTLIST transport xmlns:android CDATA #IMPLIED>
<!ATTLIST transport type (TRANSPORT_WIFI | TRANSPORT_BT | TRANSPORT_BLE | TRANSPORT_USB | transport_wifi | transport_bt | transport_ble | transport_usb) #REQUIRED>
<!ELEMENT serviceChannel EMPTY>
<!ATTLIST serviceChannel xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceChannel id CDATA #REQUIRED>
<!ATTLIST serviceChannel dataRate (LOW | HIGH | low | high) #REQUIRED>
<!ATTLIST serviceChannel priority (LOW | MEDIUM | HIGH | low | medium | high) #REQUIRED>
<!ATTLIST serviceChannel reliability (ENABLE | DISABLE | enable | disable ) #REQUIRED>
]>
After the document type definition comes the registration information. There's a few pieces of information that we need to specify. This includes the name of the application followed by the information on one or more services profiles. An application (whether consumer or provider) can contain more than one service profile. For my application one service profile is enough. The service profile needs to have a friendly name and and an id. For the name I am using speedometer
and for the id will be /system/speedometer
. The role assigned to the watch application is that of a consumer
and the version number for this application is 1.0
. The Document Type Definition specifies that a serviceTimeout
value can be defined. We are not in a scenario in which this would need to be specified (I'll explain when this would be needed momentarily). All of this information would be specified in the XML file as follows.
<resources>
<application name="SpeedService">
<serviceProfile
name="speedometer"
id="/system/speedometer"
role="consumer"
serviceTimeout="30"
serviceLimit="one_peeragent"
version="1.0"
>
</serviceProfile>
</application>
</resources>
This definition isn't complete yet. There are two more pieces of information needed; the communications transports supported by the application and a definition for atleast one serviceChannel
. A service needs at least on serviceChannel
for communication but can have more than one. For this application only one is needed. The service channel is identified by a numerical ID (I've arbitrarily chosen the value 149
). Three other properties are eneded as quality of service parameters (QoS). A dataRate
which can either be low
or high
, a priority
which can be either high
, medium
, or low
, and a reliability
that can be set to enable
or disable
. When reliability
is set to enable
if a message is lost in communication it will automatically be resent. There is some additional overhead for this. Adding these elements to the file I end up with the following. The new sections are in bold type.
<resources>
<application name="SpeedService">
<serviceProfile
name="speedometer"
id="/system/speedometer"
role="consumer"
serviceTimeout="30"
serviceLimit="any"
version="1.0"
>
<supportedTransports>
<transport type="TRANSPORT_BT" />
</supportedTransports>
<serviceChannel
id="149"
dataRate="low"
reliability="enable"
priority="low"
/>
<serviceChannel
id="150"
dataRate="low"
reliability="enable"
priority="low"
/>
</serviceProfile>
</application>
</resources>
Now that the profile has been defined we need to specify where it is in the configuration file (config.xml
). The location is specified in a tizen:metadata
element with the key
named AccessoryServicesLocation
and the value set to the path to the profile definition (/res/xml/sapservices.xml
). While we are modifying the config.xml
file there is a permission that needs to be added with a tizen:privilege
tag. This permission gives the application access to the Samsung Accessory Protocol related functionality. The tizen:privilege
tag has a single attribute name
that will need to have a value of http://developer.samsung.com/privilege/accessoryprotocol
.
="1.0" ="UTF-8"
<widget xmlns="http://www.w3.org/ns/widgets" xmlns:tizen="http://tizen.org/ns/widgets" id="http://yourdomain/Speedometer" version="1.0.0" viewmodes="maximized">
<tizen:application id="fWZfEb6GWJ.Speedometer" package="fWZfEb6GWJ" required_version="2.2"/>
<content src="index.html"/>
<feature name="http://tizen.org/feature/screen.size.all"/>
<icon src="icon.png"/>
<name>Speedometer</name>
<tizen:privilege name="http://developer.samsung.com/privilege/accessoryprotocol"/>
<tizen:metadata key="AccessoryServicesLocation" value="res/xml/sapservices.xml"/>
</widget>
Writing the Consumer's Code
The consumer's code is going to be written in JavaScript. Open /js/main.js
. There's already some code in this file. Before we change it let's add a few variables to the top of the file.
The watch/consumer is going to be responsible for initiating a connection back to the phone/provider. The connection can be made in a few steps. The watch needs to instantiate an Accessory Agent (SAAgent
) and have access to that agent's connection (SASocket
). For the connection we'll need to use the same channel ID that had been defined in the service profile earlier. I had arbitrarily chosen the value 149 for the ID number. I need to know the name used by the application on the provider side (note: The provider's code hasn't yet been written. I'm calling it SpeedService
). Lastly, the phone acquires the current speed in meters per second. I'm doing conversions to other speed units on the watch side. So I'm defining a few values that will be used for the conversion.
var SAAgent = null;
var SASocket = null;
var CHANNELID = 149;
var ProviderAppName = "SpeedService";
var MilesPerHour = {factor:2.23694, label:"MPH"};
var KilometersPerHour = {factor:3.6, label:"KPH"};
var currentUnit = KilometersPerHour;
The code that is already present will change the text that appears on the screen when some one taps the screen. Change this so that it calls the yet to be defined connect()
method instead.
$(window).load(function(){
document.addEventListener('tizenhwkey', function(e) {
if(e.keyName == "back")
tizen.application.getCurrentApplication().exit();
});
connect();
});
The next several sections of code all contribute to establishing a connection. They are chained together through a series of callback methods that get set and triggered by other methods. In the connect()
method we need to request an instance of an SAAgent
. When we request a SAAgent
one will be returned for each serviceProvider
defined in the code. Since we have only defined a single serviceProvider
an array of one element is going to be returned. The connect method will first request the SAAgent
with the method webapis.sa.requestSAAgent()
. This method takes two functions for arguments. The first function is the one to be called if the call is successful. The second function is the one to call if an error occurs. In the success function we are going to continue the connection process taking the first (and only) SAAgent
and asking it to find its peer agents on the other device with SAAgent.findPeerAgents()
. Before this method is called a callback object needs to be set on the SAAgent
with SAAgent.setPeerAgentFindListener()
.
function onsuccess (agents) {
try {
if(agents.length>0) {
SAAgent = agents[0];
SAAgent.setPeerAgentFindListener(peerAgentFindCallback);
SAAgent.findPeerAgents();
}
}catch(err) {
console.log("onsuccess exception [" + err.name + "] msg[" + err.message + "]");
}
}
function onerror(error) {
console.log("ONERROR: err [" + error.name + "] msg [" + error.message + "]");
}
function connect() {
try {
console.log("connect():requesting SAAgent (connect)");
webapis.sa.requestSAAgent(onsuccess , onerror)
} catch(err) {
console.log("onsuccess exception [" + err.name + "] msg[" + err.message + "]");
}
}
The peerAgentFindCallback
object defines two methods. The method onpeeragentfound
is called when a peerAgent
is found. The other, onerror
is called if the attempt to find peerAgent
s fails. When a peer agent is found we check to see if it is the peer agent that we are looking for by seeing if it's appName
matches the name that we were looking for (I defined it in the variable ProviderAppName
variable earlier). If it's the peer agent that I'm looking for then I provide a callback to receive the connection to it (agentCallback
) and call SAAgent.requestServiceConnection()
to receive an instance of the callback.
var peerAgentFindCallback = {
onpeeragentfound : function(peerAgent) {
try {
if(peerAgent.appName === ProviderAppName) {
SAAgent.setServiceConnectionListener(agentCallback);
SAAgent.requestServiceConnection(peerAgent);
} else {
}
}
catch(err) {
console.log("onpeeragentfound exception: [" + err.name + "] msg [" + err.message + "]");
}
},
onerror: function(error) {
console.log("peerAgentFindCallback error: err [" + error.name + "] msg [" + error.message + "]" + error);
}
};
The agentCallback
doesn't have much work to do. It receives a SASocket
that is in the connected state. A reference to it is saved and a callback is set to disconnect if the socket state changes. I then provide the callback that will receive data.
var agentCallback = {
onconnect: function (socket) {
SASocket = socket;
SASocket.setSocketStatusListener(function (reason) {
disconnect();
});
SASocket.setDataReceiveListener(onreceive); },
onerror:onerror1
};
Before we can continue we need to know what data is going to be received and how it's formatted and structured. Let's leave the watch project alone and start on the Android service.
Building the Android Service
I'm using the IntelliJ derived Android Studio to build the Android service. IntelliJ has an odd dependency behaviour when an inner classes's contructor must access the class
object of it's parent class. So my class oraganization is slightly different than what you will find in the example code that Samsung provides but the code is compatible with both IDEs. Create a new Android project targeting Android 4.3 or later. No activity is needed in this application. It will be a service only application.
Before adding any code I'd like to start off by adding some permissions to the AndroidManifest.xml
. These permissions are needed for accessing location data, using Bluetooth (needed to communicate with the watch), and for accessing some other SDK related functionality.
<!---->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!---->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.INTERNET"/>
<!---->
<uses-permission android:name="com.samsung.wmanager.APP" />
<uses-permission android:name="com.samsung.wmanager.ENABLE_NOTIFICATION" />
<uses-permission android:name="com.samsung.accessory.permission.ACCESSORY_FRAMEWORK"/>
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY"/>
<uses-permission android:name="android.permission.SET_DEBUG_APP" />
References needed to be added to two Samsung SDKs, the Samsung SDK and the Samsung Accessory SDK. The project needs a class defined for the connection to the watch and a class defined for the location service. The connection class will extend from SASocket
and the location service will extend from SAAgent
. Add a new class to the project caled SAPServiceProviderConnection
and have it inherit from SASocket
. Give the class an integer field named connectionID
and a field of type SpeedService
named mParent
. The SpeedService
class hasn't been defined yet. Don't worry if the IDE gives you any warnings about it yet.
Extending the SASocket Implementation
I've followed Samsung's lead on giving each connection a unique numeric ID value. But I'm using it differently. In Samsung's examples the ID is being used to uniquely identify a connection that is being added or removed from the list of connections. With objects being passed by reference it's possible to keep track of it without giving it a numeric ID; the objects reference alone is sufficient. The connectionID
is assigned in my code for debugging purposes only.
This extended SASocket
class doesn't need to do much. It will request to be removed from the parent object's collection of active connections when it is being closed. For this project data only needs to flow from the phone to the watch. So the SAPServiceProviderConnection
class ignores any incoming data in onReceive()
. The complete class follows.
package net.j2i.speedometer;
import android.util.Log;
import com.samsung.android.sdk.accessory.SASocket;
import java.io.IOException;
public class SAPServiceProviderConnection extends SASocket {
private int connectionID;
static int nextID = 1;
public final static String TAG = "SAPServiceProvider";
private SpeedService mParent;
public void setParent(SpeedService speedService) {
mParent = speedService;
}
public SAPServiceProviderConnection() {
super(SAPServiceProviderConnection.class.getName());
connectionID = ++nextID;
}
@Override
protected void onServiceConnectionLost(int reason) {
if(mParent!=null) {
mParent.removeConnection(this);;
}
}
@Override
public void onReceive(int channelID, byte[] data) {
}
@Override
public void onError(int channelID, String errorString, int errorCode) {
Log.e(TAG,"ERROR:"+errorString+ " | " + errorCode);
}
}
Implementing the Provider Service
Create a new class named SpeedService
. The class needs to extend from SAAgent
. The SAAgent
class extends from Service
. This class will contain the entry point for the Android program and be responsible for acquiring and broadcasting speed information. There are a few member variables in the class. There's a Binder
object (common in services), a member for a collection for the connections, and the ID of the service channel to be used There's an instance of LocationListener
within the service that encodes the location information as a JSON string to be forwarded to the phone. There's additional information in what is being communicated. Speed, bearing, and location are transmitted together along with the state of the GPS listener. This same service could be used in making an altimeter, compass, or other application for the watch.
SpeedBinder mBinder = new SpeedBinder();
public final static String TAG = "SpeedService";
public final static int SAP_SERVICE_CHANNEL_ID = 149;
AbstractCollection<SAPServiceProviderConnection> mConnectionBag = new Vector<SAPServiceProviderConnection>();
LocationManager mLocationManager;
boolean mIsListening = false;
public class SpeedBinder extends Binder {
SpeedService getService() {
return SpeedService.this;
}
}
LocationListener locationListener = new LocationListener() {
long mTime;
float mBearing, mSpeed;
double mLatitude, mLongitude, mAltitude;
boolean bHasAltitude, bHasBearing, bHasSpeed;
boolean bGpsEnabled = true;
@Override
public String toString() {
final String returnValue =
String.format("{ \"gpsEnabled\" :%b,"+
"\"hasSpeed\":%b, \"speed\":%1.2f, \"hasBearing\":%b, \"bearing\":%1.4f,"+
"\"latitude\":%f, \"longitude\":%f,\"hasAltitude\":%b, \"altitude\":%1.3f}",
bGpsEnabled,
bHasSpeed, mSpeed, bHasBearing, mBearing,
mLatitude, mLongitude, bHasAltitude, mAltitude
);
return returnValue;
}
@Override
public void onLocationChanged(Location location) {
if(bHasSpeed = location.hasSpeed())
mSpeed = location.getSpeed();
if(bHasAltitude = location.hasAltitude())
mAltitude = location.getAltitude();
if(location.hasSpeed())
mSpeed = location.getSpeed();
if(bHasBearing = location.hasBearing())
mBearing = location.getBearing();
mLatitude = location.getLatitude();
mLongitude = location.getLongitude();
mTime = location.getTime();
transmitLocation();
}
@Override
public void onStatusChanged(String s, int i, Bundle bundle) { }
@Override
public void onProviderEnabled(String s) { bGpsEnabled = true; }
@Override
public void onProviderDisabled(String s) { bGpsEnabled = false; }
};
The transmitLocation()
method converts the JSON for the current location from a String
to a byte
array and sends it to every active connection that the service has.
public void transmitLocation() {
String locationString = locationListener.toString();
byte[] locationMessage = locationString.getBytes();
Log.i(TAG, locationString);
Iterator connectionIterator = mConnectionBag.iterator();
while(connectionIterator.hasNext()) {
SAPServiceProviderConnection connection = (SAPServiceProviderConnection)connectionIterator.next();
try {
connection.send(SAP_SERVICE_CHANNEL_ID, locationMessage);
} catch(IOException exc) {
}
}
}
When connections are initiated or closed they are added or removed from the collection of connections. When a change occurs in the mConnectionBag
member the class evaluates whether or not the GPS needs to be active. When there are no active listeners there is no reason to keep the GPS hardware active; to do so would be abusive to the battery and may raise concern if it's noticed that the phone is monitoring a users' location at times when he or she isn't doing anything related to location. reevaluateLocationManager()
calls startTracking()
any time that a new connection is added to the connection collection. Calling this method when tracking is already enabled will have no negative impact; the method does nothing if tracking is active. Once the connection count reaches zero stopTracking()
is called.
public void removeConnection(SAPServiceProviderConnection connection) {
mConnectionBag.remove(connection);
reevaluateLocationManager();
}
public void addConnection(SAPServiceProviderConnection connection) {
mConnectionBag.add(connection);
transmitLocation();
}
void startTracking() {
if(!mIsListening) {
mIsListening = true;
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1000, 1, locationListener);
}
}
void stopTracking() {
if(mIsListening) {
mLocationManager.removeUpdates(locationListener);
mIsListening = false;
}
}
void reevaluateLocationManager() {
if(mConnectionBag.size()==0)
stopTracking();
else
startTracking();
}
The service should run until explicitly stopped. The services onStart
method returns START_STICKY
to let the android system know this.
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(getBaseContext(), "started", Toast.LENGTH_LONG).show();
Log.i(TAG, "started");
return START_STICKY;
}
Configuring the Provider Agent Profile
The Provider's Agent Profile will look near identical to the profile made for the consumer. Like on the Tizen side I'm placing the profile in /re/xml
. The name of the XML file will be sapservices.xml
. The differences in the content of this file and the one in the Tizen project are instead of being assigned the consumer
role it will be assigned the provider
role and a new attribute called serviceImpl
is present to point to the Java class that contains the agent implementation. This profile also has a service limit of any
allowing it to serve any number of accessory peer's that might connect to it. The consumer's limit is set to one
; it will only be acquiring speed information from a single device.
<resources>
<application name="SpeedService">
<serviceProfile
name="speedometer"
id="/system/speedometer"
role="provider"
serviceLimit="any"
version="1.0"
serviceImpl="net.j2i.speedometer.SpeedService"
>
<supportedTransports>
<transport type="TRANSPORT_BT"/>
</supportedTransports>
<serviceChannel
id="149"
dataRate="low"
reliability="enable"
priority="low"
/>
</serviceProfile>
</application>
</resources>
The last changes that needs to occur are the AndroidManifest.xml
needs to have an entry that indicates where the Service Profile is defined and the service and the intents that is responds to need to be declared.
<service android:name=".SpeedService" />
<receiver android:name = "com.samsung.android.sdk.accessory.ServiceConnectionIndicationBroadcastReceiver">
<intent-filter>
<action android:name="android.accessory.service.action.ACCESSORY_SERVICE_CONNECTION_IND"/>
</intent-filter>
</receiver>
<receiver android:name = "com.samsung.android.sdk.accessory.RegisterUponInstallReceiver">
<intent-filter>
<action android:name="android.accessory.device.action.REGISTER_AFTER_INSTALL"/>
</intent-filter>
</receiver>
<meta-data android:name="AccessoryServicesLocation" android:value="/res/xml/sapservices.xml" />
Updating the Watch's Display
At this point the watch project is able to establish a connection and receive data from the provider, but isn't doing anything with the content that it receives. Create an onReceive
function. The arguments it accepts will be a an integer for the channel ID and a string that holds that data received. When data is received I check to make sure that the data received is on the CHANNELID
that I expect. This isn't strictly necessary since data can't come in on a channel that hasn't been defined and I've only defined one. But I'm doing this should I decide to change the program later to allow for other data to be transmitted. When the location data comes in I'm taking the speed
component, passing it through a formatter/conversion function, and setting the contents of the textbox to the formatted speed.
function onreceive(channelid, data) {
console.log("received:"+data);
switch(channelid) {
case CHANNELID: {
try {
var locationData = jQuery.parseJSON(data);
$('#textbox').html( formatSpeed(locationData.speed));
}
catch(e) {
$('#textbox').html("failed to parse");
}
}
break;
default:
break;
}
}
Packaging the Watch Application and the Android Application together
Both parts of the application can be packaged together and deployed to the phone. If the compiled watch application is copied to the path /res/assets
of the Android project the Gear Manager will take care of discovering the watch application and copying it to the watch. Start the debugger so that the project deployes to the phone. Since there are no activities declared in the application you may get prompted on what to do after deployment. It's not necessary to start any activity so allow the application to deploy. A few moments after deploying the discovery of the service and the watch application will occur. The application will be deplopyed to the watch and the service will start.
Note that if you update the watch application you will want to copy the updated application to the /res/assets
folder of the service project. If you do not then any redeployment of the service will result in the watch having the version of the watch app that had been in that folder.
A speed readout on the watch.
Adding a Settings Screen
The watch applicaton was set to show speed in Kilometers per hour. The information needed to show speed in some other unit is present but there's no way to select which unit conversion is used.In the user interface I want to have a different page for changing this setting. Let's look back at the contents of the <body>
tag for the watch application.
<div class=contents>
<div style='margin:auto;'>
<span class=content_text id=textbox>waiting on gps</span>
</div>
</div>
There are CSS classes pre-defined that are used for navigation. The primary CSS classes of interest are ui-page
and ui-page-active
. Each one of the pages or views in my application will be part of the same HTML file but defined by a <div />
element with the ui-page
class. I'll mark the page that should be active by default with both ui-page
and ui-page-active
and navigation within the application will be implemented by changing the classes assigned to each page.
Much of the navigation will at first look appear to look like regular anchor tag based navigation. The href
attribute will have for it's value the id
of the <div>
to which it is navigating. Here is a simple version of a HTML content with navigation between two div elements.
<body>
<div class="ui-page ui-page-active" id="main">
<div class="ui-header" data-position="fixed"><h2>Page One</h2></div>
<div class="ui-content">
<a href="#secondPage">to second page</a>
</div>
</div>
<div class="ui-page" id="secondPage">
<div class="ui-header" data-position="fixed"><h2>Page One</h2></div>
<div class="ui-content">
</div>
</div>
</body>
Part of the mechanics of this form of navigation are implemented in the Tizen Advanced UI framework (TAU). For this to work you need to have the TAU code present in your project and referenced in your HTML. The framework will normally be in the path lib/tau/wearable/js
. If this path doesn't exists in your project you can make a second project using the "Basic" template and copy the files from there. Remember to place the references to the CSS and JavaScript for TAU in your page's header
<link rel="stylesheet" href="lib/tau/wearable/theme/default/tau.min.css">
<script type="text/javascript" src="lib/tau/wearable/js/tau.js"></script>
The settings screen will exists within the same HTML file. For now I only want to allow someone to switch between SI and imperial speed units. The speed units are already defined in the JavaScript for the page. Instead of defining it redundantly I will build part of the interface programmatically.
function populateUnits() {
var unitList = document.getElementById('unitsList');
UnitList.forEach(function(e) {
var li = document.createElement('li');
var radio = document.createElement('input');
var label = document.createElement('label');
radio.setAttribute('type','radio' );
radio.setAttribute('name', 'units');
radio.setAttribute('value', e.label);
radio.setAttribute('id', 'unit_' + e.label);
if(e.label == currentUnit.label)
radio.setAttribute('checked','true');
radio.setAttribute('onclick', 'setUnit("'+ e.label + '");');
label.innerText = e.label;
label.setAttribute('for', 'unit_', e.label );
label.innerText = e.label
li.appendChild(radio);
li.appendChild(label);
radio.setAttribute('href','#');
unitList.appendChild(li);
});
}
The resulting speed unit selection screen
Radio buttons are displayed for the available units. When a radio button is checked a call is made to a function named setUnit
that will both save the selection and change which conversion unit is used for displaying speed.
function setUnit(unit ) {
localStorage.setItem("speedUnit", unit);
applySpeedSetting();
}
function applySpeedSetting() {
var unit = localStorage.getItem('speedUnit');
if(unit!=null) {
UnitList.forEach( function(x) {
if(x.label == unit) {
currentUnit = x;
return;
}
});
}
}
The localStorage
object is described in the W3C Web Storage API and can be used for storying key/value pairs. The applySettings
function is called both in response to the selected speed unit having changed and will be called in the begining of the program's life to apply the saved selection. It iterates through the speed units that were defined in the code until it finds the unit whose label matches what was saved. Upon finding a match it sets this object as the current unit to use and stops searching.
Compatibility with the Gear S
I originally wrote this before the release of the Galaxy Gear S. Among other features the Gear S has it's own GPS radio; it should be able to detect the speed without communicating with a phone. I wanted this program to take advantage of the upcoming phones capabilities. For HTML based projects location information is exposed through the W3C Geolocation API Specification.
Within Tizen location information requires a privilege. Since this application can work with location information from the phone and location information from the watch isn't mandatory I am listing it in the program's manifest as a privilege and not a requirement. The following line has been added to the config.xml
to specify this priviledge.
<tizen:privilege name="http://tizen.org/privilege/location"/>
To use the Geolocation API first check to see if there is a geolocation
field on the navigator
object. If the field isn't found then we can't use this method and fall back on the Samsung Accessory SDK and Android service. If the object is found attempt to use it to request location updates by calling navigator.geolocation.watchPosition()
. The watchPosition
method is receiving a callback function to which location information is to be sent, a mathod to call if there is an error, and a parameter that indicates the maximum age of location information received. When location information is received it's treated the same way as information that came from the the phone. If there is any error in attempting to use the watch's GPS the program will fall back on the phone.
These changes are implemented by making the following code changes
function locationCallbackSuccess(position ) {
if(position.speed)
$('#textbox').html( formatSpeed(position.speed));
}
function locationCallbackFailed( reason ) {
connect();
}
$(window).load(function(){
document.addEventListener('tizenhwkey', function(e) {
if(e.keyName == "back") {
var currentPage = document.getElementsByClassName('ui-page-active')[0];
var pageId = (currentPage)?currentPage.id:' ';
if(pageId=='main')
tizen.application.getCurrentApplication().exit();
else {
tau.changePage("#main");
}
}
});
tau.defaults.pageTransition = "slideup";
if(navigator.geolocation) {
navigator.geolocation.watchPosition(
locationCallbackSuccess,
locationCallbackFailed,
{maximumAge:250}
);
}
else
connect();
populateUnits();
applySpeedSetting();
});
Performance and Battery
After acquiring a Galaxy Gear S I tried to use the speedometer as a stand alone application. The results were not quite what I was looking for; the Gear S seems to update location and speed information at a lower frequency than I would like. Even when I requested updated information on half second intervals I would only receive location information once ever 2 to 3 seconds. While the Gear S does have it's own GPS hardware the solution I advise is to first attempt to use the phone's hardware for location information and only fall back on the gears hardware if the phone cannot be used for location information (ex: the watch is disconnected from the phone or location services has been disable on the phone). In addition to getting a better update frequency the demands on the Gear S's battery will be lower.
Room for Improvement
Keeping in mind that the code presented here is to have something that is a step beyond a "Hello World" program there is plenty of room for improvement. If you want to polish this code to make it app store ready there are a few things you might want to consider doing. If location ceases to be sent to the watch there is no indication of the loss of information. Indicating the loss of the location information will help and reduce the chances of a user thinking that lost location information is due to fault of the program. I've displayed the speed information in plain text. There are familiar graphical ways in which it could be presented too. A user may also want the screen on the watch to stay on when this program is running instead of timing out. Lastly, try using the program yourself to see what opportunities for improvement that you discover (but please don't use it if you are operating a vehicle yourself; only use it as a passenger!).
Getting Assistance
If you've got questions or need assistance don't hesitate to post a question below. I'll try to get to it as soon as possible. You can also post questions in the Tizen Developer's Forums and the Samsung Developer's Forums.
History
- 2014 October 16 - Initial Publication
- 2014 December 3 - Added Gear S Recommendation