Introduction
Well, we keep hearing a lot about "The Internet of Things," but it's only recently that we have started to see the commercial side of this coming to fruition. Wearable devices are rapidly becoming the visible face of IoT, and Samsung has stolen an early march on the competition with its series of smart watches; first the Gear and now the Gear 2. In this article, we will cover what we need to write at the Android device side in order to develop Gear applications.
It mangles me and makes me bleed
Okay, this is where things start to get a little bit, how shall we say, interesting. Possibly the biggest pain point we have to get our heads around is that we have to develop our Gear application in two parts. By that, I mean that we are going to create our Android side in one version of Eclipse, and the Gear 2 side in another version of Eclipse. So this means that we need to a lot more planning when we are creating our application. Don't worry, it becomes fairly straightforward.
If you haven't already downloaded a version of Eclipse you can use for your Android development, I would recommend starting off by downloading and installing the Android Developer Tools from here. Don't worry, I'll wait.
Getting to the code
Ready? Good. So now we are ready to create our application. Rather than stepping through the process stage by stage, what you need to do is create a new Android Application - I called mine reputationwatcher - and set the minimum required SDK to 4.1 (Jellybean). Set the Target SDK and compilation to 4.4 (KitKat). As there are going to be no visuals or interactions on the Android side, we can remove the theme and drop the Activities.
Okay, so we've created our project, let's start adding some code. Now, it's fair to say that as I'm no Android or Gear expert, I have followed the online examples very heavily, so if you've watched those examples this should all be familiar. If you haven't, don't worry as we're going to cover the code here and we'll go into the ins and outs of what I gleaned through experimenting with the code.
There are a couple of jar files that we need to add to our libs folder. These files are accessory-v1.0.0.jar and sdk-v1.0.0.jar. To get them, we need to install the Samsung SDK from here. I used Eclipse to install my version - you'll probably want to do the same. Go to Help > Install New Software and follow the instructions to install the Samsung Accessories. When the installation is completed, copy these files from the download area into the libs folder:
Okay, let's create a new Java class. This will go into the src folder by default and will provide the basis for the actual Android side of the Gear->Android chain. We edit the package name to include provider at the end as this shows the intent of this part of the application - it will provide updates for the watch. Reflecting upon this, I called my class ReputationProvider
- feel free to call yours whatever you want, but if you do change it, remember the differences because we will be coming back to these values later on when we create our configuration.
By the end of coding our Android application, we'll have created a structure that looks something like this:
To save some time, here are the imports I use throughout this class (so let's go ahead and paste them into our .java file):
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
import android.util.SparseArray;
import android.widget.Toast;
import com.samsung.android.sdk.SsdkUnsupportedException;
import com.samsung.android.sdk.accessory.SA;
import com.samsung.android.sdk.accessory.SAAgent;
import com.samsung.android.sdk.accessory.SAPeerAgent;
import com.samsung.android.sdk.accessory.SASocket;
Our class extends the functionality in SAAgent
. SAAgent
allows our application to run on a worker thread, so it's pretty handy.
public class ReputationProvider extends SAAgent{
We'll implement several of the overrides in this class as we continue.
Remember that we talked about planning our application ahead? Well, we are now going to reap the benefit of thinking about our application. The way that the Gear communicates with the Android device is via an application defined channel. So, we're going to define the channel that we use throughout our application. Remember this number as this isn't the only place we're going to define it.
public final static int ChannelId = 2048;
Yup, that's right. I picked the channel id in honour of the game.
Right now, we need to divert and create some helper functionality. We'll create two anonymous inner classes (at least that's what some Java friends of mine tell me they are called:D) to provide access to connection handling and the binder service.
ReputationBinder
This is a simple class to implement. All it's going to do is return the current instance of ReputationProvider
. This is the class in its entirety:
public class ReputationBinder extends Binder {
public ReputationProvider getBinderService() {
return ReputationProvider.this;
}
}
ReputationSocket
Now this class is much more interesting. We're going to handle the actual socket connection in this class, so we want to start off by extending SASocket.
public class ReputationSocket extends SASocket {
The constructor for this class simply passes the name of the class back to the class it inherits from.
protected ReputationSocket() {
super(ReputationSocket.class.getName());
}
On implementing this class, we were given several overrides. We're not going to cover them all - onError
, for instance, merely logs errors and all that onServiceConnectionLost
does is remove the connection from the SparseArray
. So please, download the code and have a good look through it.
The onReceive
method, however, is where the actual "meat and potatoes" of our socket handling occurs.
@Override
public void onReceive(int channelId, byte[] data) {
final SASocket uiHandler =
connectionMap.get(connectionId);
String value = getDataFromUrl();
final byte[] outputData = value.getBytes();
new Thread(new Runnable(){
public void run() {
try {
uiHandler.send(ChannelId, outputData);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
At the start of this method, we retrieve the socket details from our connection SparseArray
(I know we haven't actually defined this yet - we'll get to that one in a minute). Next we get the data that we are interested in (again, like connectionMap
, this is actually defined in ReputationProvider
) and convert that to a byte array. This is where we start to get an inkling that this particular method is incredibly interesting - the byte array is the same type as the incoming data into the method - which gives us the clue that this method is the point at which we want to handle data coming in from the watch, so as we are going to send a response back to the watch on an incoming request, we will want handle this here. Indeed, that's exactly what our Thread does here - it queues up the data to send back out to the watch, and the eagle (or Muppet-of-your-choice) eyed among us will have noticed that reappearance of the channel id. It's almost as though we were thinking this stuff through.
Right, that's us finished with our helper classes. Let's get back to ReputationProvider
.
Whenever we establish a connection between the watch and the device, we should receive a service connection response. This is handled, cunningly enough, in the onServiceConnectionResponse
method. This method looks a little bit like this:
@Override
protected void onServiceConnectionResponse(SASocket socket, int result) {
if (result != CONNECTION_SUCCESS) return;
if (socket == null){
Log.e("ReputationProvider", "The socket is null");
return;
}
if (connectionMap== null){
connectionMap = new SparseArray<ReputationSocket>();
}
ReputationSocket connection = (ReputationSocket)socket;
connection.connectionId = (int)(System.currentTimeMillis() & 255);
connectionMap.put(connection.connectionId, connection);
Toast.makeText(getBaseContext(), "Established connection",
Toast.LENGTH_LONG).show();
}
The first thing that we are doing here is checking to make sure that we have received a valid connection and that we have a socket instance. There's no point in proceeding if we haven't succeeded with either of these conditions.
We cast the socket into the socket class that we created earlier and set the connectionId
on it, before we save this connection information for later use. There are many ways we could have allocated the connection id, but I chose to follow the examples from Samsung here. This is purely down to my lack of familiarity with the nuances of Java development, so I would expect more seasoned Java-heads to come up with a much better way than this. Finally, we raise a Toast notification to inform the user on the android side that we have established a connection.
When the watch attempts to bind, we will handle this via the onBind
method. This simply returns an instance of the ReputationBinder
that we created (the clue is in the fact that it's expecting an IBinder
to be returned and we implemented IBinder
for ReputationBinder
; cunning yes).
Okay, I mentioned the getDataFromUrl
method earlier. Well, congratulations, you just got a CP reputation boost. For this implementation, we're simply returning 10 million so you've just leapfrogged the slackers who don't read the articles:D.
Well, that about wraps it up for the Android side, so thank you for reading, I hope you enjoyed this.
"Wait Pete. I thought you said we'd come back to that 2048"
Oops, well yes, that doesn't wrap it up on the Android end - we have two more bits to add. The first bit we're going to add is the metadata describing the profile of our service. The second part we're going to add is to update the application manifest.
Okay, in the res folder, create a folder called metadata. Inside that, add an empty xml file. Let's call it repwatchermetadata.xml. Now, copy the following into it:
<!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_accessory | one_peeragent) #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>
]>
Most of it is pretty straightforward, but as you can see we are now starting to link some things together. First of all, we tell it the path to the implementation of our functionality in the serviceImpl
attribute along with details of the transport mechanisms we are going to allow. In order to test our application, we need to allow it to communicate via WIFI, so we add that element as well as the normal support for Bluetooth.
Yes. You spotted it again, there's that 2048 appearing again. This is where we expose the channel we want to use.
Well, all that's left now is to update the manifest file which looks something like this:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.codeproject.reputationwatch"
android:versionCode="1"
android:versionName="1.0" >
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="com.samsung.accessory.permission.ACCESSORY_FRAMEWORK" />
<uses-permission android:name="com.samsung.wmanager.APP" />
<uses-permission android:name="com.samsung.wmanager.ENABLE_NOTIFICATION" />
<uses-sdk
android:minSdkVersion="14"
android:targetSdkVersion="18" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<service android:name="com.codeproject.reputationwatch.provider.ReputationProvider" />
<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/metadata/repwatchermetadata.xml"
/>
</application>
</manifest>
Again, most of this is pretty straightforward. Make sure you add in the permissions and check to ensure that the path to the metadata file is set, as well as the path to the ReputationProvider
source.
Okay, now we're done. Don't close Eclipse just yet as you'll need to copy something over from the watch source here. I'll give you a preview here, when we've built the watch side of our application, we are going to copy the widget file it creates (it ends in .wgt) into the assets folder. In the next article, we'll add functionality at the watch side and cover debugging our completed application.