Saturday 2 January 2016

My First Plugin - Adding android wear features to your Nativescript app - part 2

Back in November I blogged about making my app usable on an android wear watch. At the time this was a prototype.

I have now got it working nicely with communication features from the phone to the watch, so you can select exercises on your phone and then push the selection to the watch.

All you need to do is have the application currently running and open on your phone and watch and make your selection.

To do this, I had to write some android code using Android Studio and the wearable component of google play services.

Messaging between the phone and the watch is covered in the google development documentation here. These examples are both also very useful.

Selecting Exercises on the Phone

Choosing to push selection to Watch

Previewing on Phone
Using a bit of trial and error and the blog post here as well documentation on building plugins I managed to get it working.

One class is used to send and receive messages for google play services and start the listener for messages on the watch.

 package org.nativescript.androidwear.messaging;

import android.content.Context;
import android.widget.Toast;
import android.os.Looper;

import com.google.android.gms.common.api.GoogleApiClient;
import com.google.android.gms.common.api.ResultCallback;
import com.google.android.gms.wearable.MessageApi;
import com.google.android.gms.wearable.MessageEvent;
import com.google.android.gms.wearable.Node;
import com.google.android.gms.wearable.NodeApi;
import com.google.android.gms.wearable.Wearable;
import com.google.android.gms.wearable.WearableListenerService;

import java.util.List;
import java.util.concurrent.TimeUnit;

public class SendReceiveMessage extends WearableListenerService {

    private static final long CONNECTION_TIME_OUT_MS = 100;
    private static SendReceiveMessageListener onMessageReceivedCallback = null;
    private static GoogleApiClient client;
    private static SendReceiveMessage listener;
    private static boolean notifyToast = false;

    @Override
    public void onMessageReceived(MessageEvent messageEvent) {
        receive(this, messageEvent.getPath(), new String(messageEvent.getData()));
    }

    public static void registerListener(SendReceiveMessageListener listener)
    {
        onMessageReceivedCallback = listener;
    }

    /**
     * Receive message, displaying as a toast if no action set, otherwise running the action
     */

    public static void receive(Context context, String messagePath, String  messageReceived) {
        if (onMessageReceivedCallback == null) {
            if (notifyToast) {
                Toast.makeText(context, messagePath + " received no callback", Toast.LENGTH_LONG).show();
            }
        }
        else
        {
            if (notifyToast) {
                Toast.makeText(context, messagePath + " received with callback", Toast.LENGTH_LONG).show();
            }
            onMessageReceivedCallback.receive(messagePath,messageReceived);
        }
    }

    /**
     * Start listening for messages
     */

    public static void startListener(Context context, boolean notify)
    {
        // Get Client

        client =  new GoogleApiClient.Builder(context)
                .addApi(Wearable.API)
                .build();

        listener = new SendReceiveMessage();
        Wearable.MessageApi.addListener(client,listener);
        notifyToast = notify;
        if (notify) {
            Toast.makeText(context, "Listener started!!", Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Stop listening for messages
     */

    public static void stopListener(Context context, boolean notify)
    {
        Wearable.MessageApi.removeListener(client,listener);
        if (notify) {
            Toast.makeText(context, "Listener stopped!!", Toast.LENGTH_LONG).show();
        }
    }

    /**
     * Sends a message to the connected mobile device, telling it to show a Toast.
     */
    public static void send(final Context context, final String messagePath, final String messageToSend, final boolean notify) {

        // Get Client

        final GoogleApiClient client =  new GoogleApiClient.Builder(context)
                .addApi(Wearable.API)
                .build();

        new Thread(new Runnable() {
            @Override
            public void run() {

                // Get First Node

                client.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
                NodeApi.GetConnectedNodesResult result =
                        Wearable.NodeApi.getConnectedNodes(client).await();
                List<Node> nodes = result.getNodes();
                if (nodes.size() > 0) {
                    final String nodeId = nodes.get(0).getId();

                    // Send Message

                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            client.blockingConnect(CONNECTION_TIME_OUT_MS, TimeUnit.MILLISECONDS);
                            Wearable.MessageApi.sendMessage(client, nodeId, messagePath, messageToSend.getBytes()).setResultCallback(new ResultCallback<MessageApi.SendMessageResult>() {
                                @Override
                                public void onResult(MessageApi.SendMessageResult sendMessageResult) {
                                    if (!sendMessageResult.getStatus().isSuccess()) {
                                        Looper.prepare();
                                        Toast.makeText(context, "Message not sent - failure", Toast.LENGTH_LONG).show();
                                        Looper.loop();
                                    }
                                }
                            });
                            client.disconnect();

                            if (notify) {
                                Looper.prepare();
                                Toast.makeText(context, "Message sent", Toast.LENGTH_LONG).show();
                                Looper.loop();
                            }
                        }
                    }).start();
                }
                else
                {
                    if (notify) {
                        Looper.prepare();
                        Toast.makeText(context, "Message not sent - no nodes", Toast.LENGTH_LONG).show();
                        Looper.loop();
                    }
                }
                client.disconnect();

            }
        }).start();
    }
}
   

The second file is an interface, used as a bridging mechanism between the android code and nativescript.
package org.nativescript.androidwear.messaging;

/**
 * Created by Peter on 1/01/2016.
 */
public interface SendReceiveMessageListener {
    void receive(final String messagePath, final String messageReceived);
}
The build.gradle file is set up to build a library file.

apply plugin: 'com.android.library'

android {
    compileSdkVersion 23
    buildToolsVersion "21.1.2"

    defaultConfig {
        minSdkVersion 17
        targetSdkVersion 22
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile 'com.google.android.gms:play-services-wearable:8.4.0'
}

Once built you can use the library aar file in plugin.

nativescript-wear-messaging
--platforms
----android
------libs
--------wearmessaging.aar
-------AndroidManifest.xml
-------include.gradle
--index.js
-- package.json

The typescript version of the index.js file is
var app = require('application');
var context = app.android.context;

// If notify is turned on, it will display debug toast messages for testing

var notify = false;

// Call this first in the page you want to use the messaging in

export function startListener() {
    try {
        org.nativescript.androidwear.messaging.SendReceiveMessage.startListener(context, notify);
    }
    catch (ex) {
        alert(ex.message);
    }
}

// Call this when the page is closed

export function stopListener() {
    try {
        org.nativescript.androidwear.messaging.SendReceiveMessage.stopListener(context, notify);
    }
    catch (ex) {
        alert(ex.message);
    }
}

// Pass in a callback in the page you want to listen for messages, you can JSON.parse to convert strings back into objects

export function receive(receiveCallback: (messagePath: string, messageReceived: string) => void) {
    try {
        var sendReceiveMessageListener = new org.nativescript.androidwear.messaging.SendReceiveMessageListener({ receive: receiveCallback });
        org.nativescript.androidwear.messaging.SendReceiveMessage.registerListener(sendReceiveMessageListener);
    }
    catch (ex) {
        alert(ex.message);
    }
}

// Call this from your phone app, the message path is the keyname of the message, you can use JSON.stringify to convert objects to be sent

export function send(messagePath: string, messageToSend: string) {
    try {
        org.nativescript.androidwear.messaging.SendReceiveMessage.send(context, messagePath, messageToSend, notify);
    }
    catch (ex) {
        alert(ex.message);
    }
}
The manifest file needs to contain a reference to the custom WearListenerService. This will then be copied into the overall AndroidManifest file.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    android:versionCode="1"
    android:versionName="1.0" >
 <application>
  <service
            android:name="org.nativescript.androidwear.messaging.SendReceiveMessage" >
            <intent-filter>
                <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
            </intent-filter>
        </service>
 </application>
</manifest>
The include gradle needs to include detail on the dependencies
//default elements
android { 
    productFlavors {
        "nativescript-wear-messaging" {
            dimension "nativescript-wear-messaging"
        }
    }
}

//optional elements
dependencies {
compile 'com.android.support:appcompat-v7:23.1.1'
    compile "com.google.android.gms:play-services-wearable:8.4.0"
}
The plugin needs to be installed on the mobile phone and watch versions of the code. Messages can be sent to/from each device. A snippet of the code for the model can be seen below.
         // Called on page load

    public startListenerReload() {
        try {

            // Start listening for messages
           
            wearMessaging.startListener();
            
            // When message is received process then

            wearMessaging.receive(

                (messagePath: string, messageReceived: string) => {
                    try {

                        // Check message is the one we are interested in, store it in application settings
                        // Convert to JSON object and display on screen

                        if (messagePath == this.messagingPath) {
                            appSettings.setString("exercises", messageReceived);
                            var exercises = <IExercises>JSON.parse(messageReceived);
                            this.loadData(exercises);
                        }
                    }
                    catch (ex) {
                        alert(ex.message);
                    }
                }
            );
            
            // load previously loaded messages

            var messageReceived = appSettings.getString("exercises", "");
            if (messageReceived != "") {
                var exercises = <IExercises>JSON.parse(messageReceived);
                this.loadData(exercises);
            }
        }
        catch (ex) {
            alert(ex.message);
        }
    }

    // Send message to other device

    public sendMessages(exercises: IExercises) {
        wearMessaging.send(this.messagingPath, JSON.stringify(exercises));
    }

No comments:

Post a Comment