Developing Garmin SmartWatch Apps with ConnectIQ Platform-Part 3

In this third and last part of this tutorial we shall see how to pass the meeting information from the Android app to the SmartWatch app/widget that we developed in part 2 of this tutorial. This part of the tutorial requires some prior knowledge on Android development. If you are in need of a quick android tutorial then you can find it here.

ConnectIQ platform comes with following pre-built mobile SDKs.
  1. Android BLE - (actually connects Android app to Smartwatch Apps over BLE)
  2. iOS BLE - (connects an iOS mobile app to Smartwatch App over BLE)
  3. Android ADB - (used to simulate BLE connection over ADB)
These SDK's  are set of libraries  required to communicate over BLE or to simulate a BLE like connection over ADB (the Android debug bridge). In this part of the tutorial we will look at that Android ADB SDK so that we can simulate the BLE like connection using the emulator in our development environment.

You can select and download the Android ADB SDK from the Mobile SDK download page from the link given below.


The SDK file would be a zip file named like connectiq-mobile-sdk-android-1.3.zip. Unzip this file and you should see a JAR file inside and a sample app. That JAR file is your SDK library which has everything you need to communicate with your Garmin Smartwatch app. The sample project in the zip file shows and example of how to use this SDK.  There should be an additional file called MobileSDK.html, I would recommend going through it to understand the lifecycle events of the ConnectIQ Mobile SDK.

Import the sample project present in the zip file into Android Studio. You should see a structure like this.

You can see the libs folder containing the connectiq.jar and under /src you should see a few classes like MainActivity.java etc. I would recommend you to connect your android device and deploy this sample app as is and see what happens.

You can see that the method onSDKReady() in MainActivity.java is called after the SDK is initialized, which invokes method called loadDevices() that looks like below
 
 public void loadDevices() {
        // Retrieve the list of known devices
        try {
            List devices = mConnectIQ.getKnownDevices();

            if (devices != null) {
                mAdapter.setDevices(devices);

                // Let's register for device status updates.  By doing so we will
                // automatically get a status update for each device so we do not
                // need to call getStatus()
                for (IQDevice device : devices) {
                    mConnectIQ.registerForDeviceEvents(device, mDeviceEventListener);
                }
            }

        } catch (InvalidStateException e) {
            // This generally means you forgot to call initialize(), but since
            // we are in the callback for initialize(), this should never happen
        } catch (ServiceUnavailableException e) {
            // This will happen if for some reason your app was not able to connect
            // to the ConnectIQ service running within Garmin Connect Mobile.  This
            // could be because Garmin Connect Mobile is not installed or needs to
            // be upgraded.
            if( null != mEmptyView )
                mEmptyView.setText(R.string.service_unavailable);
        }
    }

To get the list of connected devices do the following changes in MainActivity.java onCreate() method.

  1. Obtain the ConnectIQ object instance using the following method 
  2. mConnectIQ = ConnectIQ.getInstance(IQCommProtocol.ADB_SIMULATOR);
    
  3. Connect and deploy the app to your phone.
  4. Execute following command on the command-line 
    1.  adb forward tcp:7381 tcp:7381
The last command would forward the TCP port to the Android device in a terminal or console allowing the simulator to communicate over ADB simulating a BLE connection.

Doing this should display the list of connected devices in the sample app after deploying it on your android mobile.

The other class DeviceActivity.java implements a View.OnClickListener interface which implements an onListItemClick() method. You can see that the mobile app tries to send a text message to the smart watch connected device by calling 
mConnectIQ.sendMessage(mDevice, mMyApp, message, new IQSendMessageListener() {

         @Override
         public void onMessageStatus(IQDevice device, IQApp app, IQMessageStatus status) {
               Toast.makeText(DeviceActivity.this, status.name(), Toast.LENGTH_SHORT).show();
             }

         });
We can replace these set of lines to pass a set of text messages that contains comma separated calendar events by calling the syncCalendarMessages() method as described below.


private void syncCalendarMessages() {
        HashMap&ltString, List&ltCalendarEvent&gt&gt eventMap = CalendarService.readCalendar(getApplicationContext(), 2, 0);
        List&ltMap&gt eventDataList = new ArrayList&lt&gt();
        HashMap&ltInteger, String&gt eventDataMap ;
        for(Map.Entry&ltString, List&ltCalendarEvent&gt&gt e:eventMap.entrySet())
        {
            for(CalendarEvent event:e.getValue())
            {
                    eventDataMap = new HashMap();
                    StringBuilder eventBuilder = new StringBuilder();

                    eventDataMap.put(1, "1223");
                    //eventDataMap.put(2, event.getBegin().toGMTString());
                    String title= event.getTitle().replaceAll(":", " ").replaceAll(",", "");
                    String location = event.getLocation().replaceAll(":", " ").replaceAll(",",  "");
                    eventBuilder.append(new SimpleDateFormat("HHmm").format(event.getBegin()));
                    eventBuilder.append(",");
                    eventBuilder.append(title);
                    eventBuilder.append(",");
                    eventBuilder.append(location);
                    eventBuilder.append(",");
                    eventBuilder.append(SIMPLE_DATE_FORMAT.format(event.getBegin()));

                Log.i("Event Map", eventBuilder.toString());
                IQMessageStatus status = null;
                try {

                    status = mConnectIQ.sendMessage(mDevice, mApp, eventBuilder.toString());
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
                if (status != IQMessageStatus.SUCCESS) {
                    //displayMessage(String.format(getString(R.string.message_send_error_format), status.name()));
                } else {
                   // displayMessage(getString(R.string.message_sent));
                   // mMessageInput.setText("");
                }
                    eventDataList.add(eventDataMap);

               // } catch (JSONException e1) {
//                    e1.printStackTrace();
//                }
            }
        }
        Log.i("CalenderEvents", eventDataList.toString());
        

    }

The CalendarService Class uses Android utility class android.provider.CalendarContract to read calendar events from the Google Calendar App.


package com.garmin.android.apps.connectiq.sample;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.CalendarContract;
import android.text.format.DateUtils;



public class CalendarService {

    // Default constructor
    public static void readCalendar(Context context) {
        readCalendar(context, 1, 0);
    }

    // Use to specify specific the time span
    public static HashMap&ltString, List&ltCalendarEvent&gt&gt readCalendar(Context context, int days, int hours) {

        ContentResolver contentResolver = context.getContentResolver();

        // Create a cursor and read from the calendar (for Android API below 4.0)
//        final Cursor cursor = contentResolver.query(Uri.parse("content://com.android.calendar/calendars"),
//                (new String[] { "_id", "displayName", "selected" }), null, null, null);

   Cursor cursor = contentResolver.query(Uri.parse("content://com.android.calendar/events"),
   new String[]{ "calendar_id", "title", "description", "dtstart", "dtend", "eventLocation" },
   null, null, null);

        // Create a set containing all of the calendar IDs available on the phone
        HashSet&ltString&gt calendarIds = getCalenderIds(cursor);

        // Create a hash map of calendar ids and the events of each id
        HashMap&ltString, List&ltCalendarEvent&gt&gt eventMap = new HashMap&ltString, List&ltCalendarEvent&gt&gt();

        // Loop over all of the calendars
        for (String id : calendarIds) {

            // Create a builder to define the time span
            Uri.Builder builder = Uri.parse("content://com.android.calendar/instances/when").buildUpon();
            long now = new Date().getTime();

            // create the time span based on the inputs
            ContentUris.appendId(builder, now - (DateUtils.DAY_IN_MILLIS * days) - (DateUtils.HOUR_IN_MILLIS * hours));
            ContentUris.appendId(builder, now + (DateUtils.DAY_IN_MILLIS * days) + (DateUtils.HOUR_IN_MILLIS * hours));

            // Create an event cursor to find all events in the calendar
            Cursor eventCursor = contentResolver.query(builder.build(),
                    new String[]  { "title", "begin", "end", "allDay", "eventLocation"},  CalendarContract.Instances.CALENDAR_ID+"=" + id,
                    null, "startDay ASC, startMinute ASC");

            System.out.println("eventCursor count="+eventCursor.getCount());

            // If there are actual events in the current calendar, the count will exceed zero
            if(eventCursor.getCount()>0)
            {

                // Create a list of calendar events for the specific calendar
                List eventList = new ArrayList();

                // Move to the first object
                eventCursor.moveToFirst();

                // Create an object of CalendarEvent which contains the title, when the event begins and ends,
                // and if it is a full day event or not
                CalendarEvent ce = loadEvent(eventCursor);

                // Adds the first object to the list of events
                eventList.add(ce);

                System.out.println(ce.toString());

                // While there are more events in the current calendar, move to the next instance
                while (eventCursor.moveToNext())
                {

                    // Adds the object to the list of events
                    ce = loadEvent(eventCursor);
                    eventList.add(ce);

                    System.out.println(ce.toString());

                }

                Collections.sort(eventList);
                eventMap.put(id, eventList);

                System.out.println(eventMap.keySet().size() + " " + eventMap.values());

            }
        }
        return eventMap;
    }

    // Returns a new instance of the calendar object
    private static CalendarEvent loadEvent(Cursor csr) {
        return new CalendarEvent(csr.getString(0),
                new Date(csr.getLong(1)),
                new Date(csr.getLong(2)),
                !csr.getString(3).equals("0"),csr.getString(4));
    }

    // Creates the list of calendar ids and returns it in a set
    private static HashSet&ltString&gt getCalenderIds(Cursor cursor) {

        HashSet&ltString&gt calendarIds = new HashSet&ltString&gt();

        try
        {

            // If there are more than 0 calendars, continue
            if(cursor.getCount() > 0)
            {

                // Loop to set the id for all of the calendars
                while (cursor.moveToNext()) {

                    String _id = cursor.getString(0);
                    String displayName = cursor.getString(1);
                    Boolean selected = !cursor.getString(2).equals("0");

                    System.out.println("Id: " + _id + " Display Name: " + displayName + " Selected: " + selected);
                    calendarIds.add(_id);
                }
            }
        }

        catch(AssertionError ex)
        {
            ex.printStackTrace();
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }

        return calendarIds;

    }
}
The CalendarEvent class is a POJO that defines fields of a calendar event and it looks like
package com.garmin.android.apps.connectiq.sample;

/**
 * Created by asoni on 22-07-2015.
 */
import java.util.Date;

public class CalendarEvent implements Comparable&ltCalendarEvent&gt{

    private String title, location;
    private Date begin, end;
    private boolean allDay;

    public CalendarEvent() {

    }

    public CalendarEvent(String title, Date begin, Date end, boolean allDay, String location) {
        setTitle(title);
        setBegin(begin);
        setEnd(end);
        setAllDay(allDay);
        setLocation(location);
    }

    public String getLocation() {
        return title;
    }

    public void setLocation(String location) {
        this.location = location;
    }
    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public Date getBegin() {
        return begin;
    }

    public void setBegin(Date begin) {
        this.begin = begin;
    }

    public Date getEnd() {
        return end;
    }

    public void setEnd(Date end) {
        this.end = end;
    }

    public boolean isAllDay() {
        return allDay;
    }

    public void setAllDay(boolean allDay) {
        this.allDay = allDay;
    }

    @Override
    public String toString(){
        return getTitle() + " " + getBegin() + " " + getEnd() + " " + isAllDay();
    }

    @Override
    public int compareTo(CalendarEvent other) {
        // -1 = less, 0 = equal, 1 = greater
        return getBegin().compareTo(other.begin);
    }

}

That is all you need once you have these classes in place then redeploy your sample application, select appropriate smartwatch device from the list and your smartwatch app should receive the calendar events and start alerting you about it.

 P.S: If you restart the ADB while redeploying then you would need to fire the following command again on your terminal/command-line in order to start communicating over ADB simulating the BLE

                                  adb forward tcp:7381 tcp:7381

For making you application production ready you would want to test the communication on real BLE  instead of simulated BLE over ADB. To do that just change back the way you obtain the ConnectIQ object instance to

mConnectIQ = ConnectIQ.getInstance(this, IQConnectType.WIRELESS);

in MainActivity.java onCreate() method and test it after deploying without being in debug mode.

Comments

  1. This is an informative blog. Keep it up. I am looking forward to this kind of blog. I took a lot away from this blog. also, your thoughts were very well organized as far as how you went into details and made it very. Thanks
    visit site

    ReplyDelete
  2. Very nice until the lesson2. Lack of knowlage of Android.... so, my learning end of Lesson 2..... Do you have more examples ? thanks for the charing

    ReplyDelete

Post a Comment

Popular posts from this blog

Developing Garmin SmartWatch Apps with ConnectIQ Platform-Part 1

Web Service Security and Native Mobile App User Authentication