Skip to content

Instantly share code, notes, and snippets.

@jasonsalas
Last active August 29, 2015 14:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonsalas/40137d7a6663da831431 to your computer and use it in GitHub Desktop.
Save jasonsalas/40137d7a6663da831431 to your computer and use it in GitHub Desktop.
Android Wear Message API (wearable-to-handheld communication)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".CommonWaterVolumesActivity"
tools:deviceIds="wear_square">
<android.support.wearable.view.WearableListView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/volumeListView" />
</LinearLayout>
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.wearable.view.WearableListView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.ArrayList;
public class CommonWaterVolumesActivity extends Activity implements WearableListView.ClickListener {
// private static final String TAG = CommonWaterVolumesActivity.class.getSimpleName();
private static final String TAG = "WaterLogg";
private WearableListView listView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.common_water_volumes);
listView = (WearableListView) findViewById(R.id.volumeListView);
listView.setAdapter(new WaterVolumeListAdapter(CommonWaterVolumesActivity.this));
listView.setClickListener(CommonWaterVolumesActivity.this);
}
private static ArrayList<String> listItems;
static {
listItems = new ArrayList<String>();
listItems.add("8");
listItems.add("12");
listItems.add("16");
listItems.add("20");
listItems.add("34");
listItems.add("40");
listItems.add("51");
}
@Override
public void onClick(WearableListView.ViewHolder holder) {
String selection = holder.itemView.getTag().toString();
Intent resultWaterVolume = new Intent();
resultWaterVolume.putExtra("selectedWaterVolume", selection);
setResult(RESULT_OK, resultWaterVolume);
finish();
}
@Override
public void onTopEmptyRegionClick() { }
private class WaterVolumeListAdapter extends WearableListView.Adapter {
private final LayoutInflater inflater;
private WaterVolumeListAdapter(Context context) {
inflater = LayoutInflater.from(context);
}
@Override
public void onBindViewHolder(WearableListView.ViewHolder holder, int position) {
TextView textView = (TextView) holder.itemView.findViewById(R.id.water_volume);
textView.setText(listItems.get(position));
holder.itemView.setTag(textView.getText());
}
@Override
public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return new WearableListView.ViewHolder(inflater.inflate(R.layout.water_volumes_layout, null));
}
@Override
public int getItemCount() {
return listItems.size();
}
}
}
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="wearables.jasonsalas.com.wearablemessagingdemo">
<!--
MANIFEST FOR THE HANDHELD APPLICATION
-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.VIBRATE" />
<application android:allowBackup="true" android:label="@string/app_name"
android:icon="@drawable/ic_launcher" android:theme="@style/AppTheme">
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
<service android:name=".ListenerService">
<intent-filter>
<action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
</intent-filter>
</service>
</application>
</manifest>
package wearables.jasonsalas.com.wearablemessagingdemo;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.NotificationManagerCompat;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.WearableListenerService;
public class ListenerService extends WearableListenerService {
/* SERVICE ON HANDHELD */
private static final String MESSAGE_PATH = "/updatefitbit";
private static final int NOTIFICATION_ID = 001;
@Override
public void onMessageReceived(MessageEvent messageEvent) {
if(messageEvent.getPath().equals(MESSAGE_PATH)) {
updateFitbit(new String(messageEvent.getData()));
}
}
private void updateFitbit(final String volume) {
new Thread(new Runnable() {
@Override
public void run() {
/* ...DO SOMETHING HERE TO PROCESS THE DATA... */
/* show a notification as a visual confirmation that the transaction went through */
NotificationCompat.Builder builder = new NotificationCompat.Builder(ListenerService.this)
.setSmallIcon(R.drawable.fitbitwhite)
.setContentTitle("Fitbit has been updated!")
.setContentText(String.format("You have logged %s fluid ounces of water in Fitbit!", volume));
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(ListenerService.this);
notificationManager.notify(NOTIFICATION_ID, builder.build());
}
}).start();
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- LAYOUT ON THE WEARABLE -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
tools:context=".WearableMainActivity"
tools:deviceIds="wear_square"
android:background="@drawable/fitbit">
<Button
android:id="@+id/btn_voice"
android:text="@string/voice_prompt"
android:layout_width="90dp"
android:layout_height="match_parent"
android:textSize="17dp" />
<Button
android:id="@+id/btn_list"
android:text="@string/list_pronpt"
android:layout_width="90dp"
android:layout_height="match_parent"
android:textSize="17dp" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/water_volume"
android:textAppearance="?android:attr/textAppearanceMedium"
android:text="Medium text"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="wearables.jasonsalas.com.wearablemessagingdemo" >
<!--
MANIFEST FOR THE WEARABLE APPLICATION
-->
<uses-feature android:name="android.hardware.type.watch" />
<application
android:allowBackup="true"
android:icon="@drawable/fitbit"
android:label="@string/app_name">
<!-- Google Play Services -->
<meta-data android:name="com.google.android.gms.version" android:value="@integer/google_play_services_version" />
<activity
android:name=".WearableMainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- system confirmation animation sequence -->
<activity android:name="android.support.wearable.activity.ConfirmationActivity" />
</application>
</manifest>
package wearables.jasonsalas.com.wearablemessagingdemo;
import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Vibrator;
import android.speech.RecognizerIntent;
import android.support.wearable.activity.ConfirmationActivity;
import android.support.wearable.view.WatchViewStub;
import android.util.Log;
import android.view.View;
import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class WearableMainActivity extends Activity {
/* ACTIVITY ON THE WEARABLE */
private static final String TAG = WearableMainActivity.class.getSimpleName();
private static final long CONNECTION_TIMEOUT = 30;
private static final int SPEECH_REQUEST_CODE = 001;
private static final int CONFIRMATION_CODE = 002;
private static final String MESSAGE_PATH = "/updatefitbit";
private GoogleApiClient client;
private String nodeId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_wearable_main);
client = new GoogleApiClient.Builder(this)
.addApi(Wearable.API)
.build();
getConnectedNodeId();
final WatchViewStub stub = (WatchViewStub) findViewById(R.id.watch_view_stub);
stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
@Override
public void onLayoutInflated(WatchViewStub stub) {
setupWidgets();
}
});
}
private void setupWidgets() {
findViewById(R.id.btn_voice).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent speechRecognizer = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
speechRecognizer.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
startActivityForResult(speechRecognizer, SPEECH_REQUEST_CODE);
}
});
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case SPEECH_REQUEST_CODE:
if(resultCode == RESULT_OK) {
ArrayList<String> results = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS);
String spokenText = (results.size() > 0) ? results.get(0) : "No speech data available";
Log.i(TAG, String.format("SPOKEN TEXT: %s", spokenText));
// send the data to the handheld to post to Fitbit via Temboo
byte[] message = spokenText.getBytes();
sendWaterVolume(message);
displayConfirmation();
}
break;
case CONFIRMATION_CODE:
// vibrate the watch once for a half-second
Vibrator vibrator = (Vibrator) getSystemService(VIBRATOR_SERVICE);
vibrator.vibrate(500);
break;
}
super.onActivityResult(requestCode, resultCode, data);
}
/*
* run the confirmation animation sequence
* */
private void displayConfirmation() {
Intent intent = new Intent(this, ConfirmationActivity.class);
intent.putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE, ConfirmationActivity.SUCCESS_ANIMATION);
intent.putExtra(ConfirmationActivity.EXTRA_MESSAGE, getString(R.string.confirmation_message));
startActivityForResult(intent, CONFIRMATION_CODE);
}
/*
* returns the first Node ID found of any connected devices
* */
private void getConnectedNodeId() {
new Thread(new Runnable() {
@Override
public void run() {
client.blockingConnect(CONNECTION_TIMEOUT, TimeUnit.SECONDS);
NodeApi.GetConnectedNodesResult result = Wearable.NodeApi.getConnectedNodes(client).await();
List<Node> nodes = result.getNodes();
if(nodes.size() > 0) {
nodeId = nodes.get(0).getId();
}
client.disconnect();
}
}).start();
}
/*
* sends the speech data to the connected device
* */
private void sendWaterVolume(final byte[] volume) {
if(nodeId != null) {
new Thread(new Runnable() {
@Override
public void run() {
client.blockingConnect(CONNECTION_TIMEOUT, TimeUnit.SECONDS);
Wearable.MessageApi.sendMessage(client, nodeId, MESSAGE_PATH, volume);
client.disconnect();
}
}).start();
}
}
}
@jasonsalas
Copy link
Author

This is a pattern for the shell of a wearable application using the Message API for Android Wear - in this case, to send voice data to the Fitbit API. A user launches the app on the watch, speaks their input, which is then transcribed and handed off to the handheld application for cloud processing.

See the project's full repo at https://github.com/jasonsalas/WaterLoggforWear

@jasonsalas
Copy link
Author

I've added support for touch, for cases when speaking the volume of water the user has consumed in public isn't feasible (i.e., in line at the bank, in court, in a crowded room, in church, etc.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment