Wednesday, October 19, 2016

Developing Garmin SmartWatch Apps with ConnectIQ Platform-Part 2

This is part 2 of blog series that covers a tutorial on developing Wearables App for Garmin SmartWatches. In this part of the tutorial we will write the logic to display Meeting Alerts in the watch widget that we created in part-1 of this tutorial.

Lets create a Monkey-C class called MeetingAlert.mc in the CalendarAlerts ConnectIQ Eclipse Project. MeetingAlert class will represent a single MeetingAlert record that we will receive from the Android Device over BLE.

using Toybox.System as Sys;
class MeetingAlert {

  //mTime - meeting time
  //mDate - meeting date
  //mSubject - meeting subject
  //mLocation -meeting location
 hidden var mTime, mDate, mSubject, mLocation;

 
 function getMeetingTime() {
  return mTime;
 }
 
 function getMeetingDate() {
  return mDate;
 }
 
 function getMeetingSubject() {
  return mSubject;
 }
 
 function getMeetingLocation() {
  return mLocation;
 }
 
 
}

We can assume that the calendar event details are received from Android Device whenever the watch synchronizes with phone over bluetooth. As mentioned in part-1 of this blog we will use comma separated string values to exchange data. Hence we are assuming that the Android Device will send us the calendar events in the following CSV format

           mTime, mSubject, mLocation, mDate

Where
    mTime - is the meeting time
    mDate - is the meeting date
    mSubject - is the meeting subject
    mLocation - is the meeting location

When we receive a message containing the above mentioned CSV string we will parse it and create a MeetingAlert Object. So to parse it we will add a method called initialize() in MeetingAlerts which will accept a CSV string of meeting record and populate the mTime, mDate, mSubject and mLoaction variables in the MeetingAlert Class.

function initialize(eventData) {
     var i=eventData.find(",");
        
      mTime = eventData.substring(0, i);
      var leftOver = eventData.substring(i+1, eventData.length());
      i=leftOver.find(",");
      mSubject =leftOver.substring(0, i);
      
      leftOver = leftOver.substring(i+1, leftOver.length());
      i=leftOver.find(",");
      mLocation =leftOver.substring(0, i);
     
      leftOver = leftOver.substring(i+1, leftOver.length());
      mDate =leftOver;
      Sys.println("mTime" + mTime +" mDate "+ mDate + " mSubject "+ mSubject+" mLocation "+mLocation);
   
 }

In the above method we are trying to parse the comma separated calendar event record String (eventData) and assign the comma separated first value to mTime, second value to mSubject, third value to mLocation and fourth value to mDate variable.

Now lets expand the folder named "resources" in our CalendarAlerts project. You can see that it has an "images" folder and a "layouts" folder. The images folder is the place where you keep all your
 the images that you would like to use in your app. and the layouts folder has a file called layout.xml which defines the layout of the UI components that you will use in your app. The project structure at this point would look as in the screenshot on the left.
Now if you open the layouts.xml you can see a bitmap tag which has a file name as shown below.

bitmap filename="../images/monkey.png

This bitmap tag is responsible to render the image of monkey in the center of the default view that you see when the app is launched(check by launching in simulator). Since we don't need it for our app we can remove it. 
Now open the CalendarAlertsApp.mc class. This is our main application class which extends Toybox::Application::AppBase and is the entry point of our application. Notice that ToyBox.Application is aliased as App in the first statement using Toybox.Application as App; This is very similar to import statements that you use to import Classes/Packages in other languages like Java. See class definition below.

using Toybox.Application as App;

class CalendarAlertsApp extends App.AppBase {

    //! onStart() is called on application start up
    function onStart() {
    }

    //! onStop() is called when your application is exiting
    function onStop() {
    }

    //! Return the initial view of your application here
    function getInitialView() {
        return [ new CalendarAlertsView() ];
    }

}

As mentioned in the code comments you can do operation that you want to do on startup of the app in the onStart() method. If you want to perform an operation on exiting the application, that needs to be written in the onStop() method.  The function getInitialView returns the first view to be shown to the app user, in our case it is the CalendarAlertsView.

Now lets create CalendarAlertsView and add something to it. The basic structure of CalendarAlertsView.mc will look like this
using Toybox.WatchUi as Ui;
using Toybox.WatchUi as Ui;
using Toybox.Graphics as Gfx;
using Toybox.System as Sys;
using Toybox.Communications as Comm;
using Toybox.Attention as Attention;
using Toybox.Timer as Timer;
using Toybox.Lang as Lan;
using Toybox.Application as App;
using Toybox.Time as Time;
using Toybox.Time.Gregorian as Calendar;
 
class CalendarAlertsView extends Ui.View {
 
     //a string message to be shown on the screen
       var displayString ="None in next 15 mins";
    
   //! Load your resources here
    function onLayout(dc) {
        setLayout(Rez.Layouts.MainLayout(dc));
    }
 
    //! Called when this View is brought to the foreground. Restore
    //! the state of this View and prepare it to be shown. This includes
    //! loading resources into memory.
    function onShow() {
    }
 
    //! Update the view
    function onUpdate(dc) {
        // Call the parent onUpdate function to redraw the layout
              View.onUpdate(dc);
             
         //Set the foreground and background color of graphics    
              dc.setColor( Gfx.COLOR_WHITE, Gfx.COLOR_TRANSPARENT );
         //Set text that we want to draw at a given top,left location on the screen
  dc.drawText(10, 20,  Gfx.FONT_MEDIUM, "Upcoming Meetings:", Gfx.TEXT_JUSTIFY_LEFT);
dc.drawText(10, 50,  Gfx.FONT_MEDIUM, displayString, Gfx.TEXT_JUSTIFY_LEFT);
       
         //Refresh the UI with changes that we did above
             Ui.requestUpdate();
    }
 
    //! Called when this View is removed from the screen. Save the
    //! state of this View here. This includes freeing resources from
    //! memory.
    function onHide() {
    }
 
}

This should display an output like as shown in the image below.
I am assuming that the comments in the source code are self explanatory. As you can see that in the onUpdate() method we are trying to draw texts on the UI using dc.drawText(). Here the object dc represent the Device Context of the wearable device and is an object of Class: Toybox::Graphics::Dc. This class has few more methods to draw different kind of graphical shapes or text. In the end of this method we requested a UI update by calling UI.requestUpdate() which indirectly request the UI update from the current View by calling onUpdate(). At this point we are left with receiving the calendar data from mobile and displaying it 15 mins prior to the meeting time in our watch. Now lets set up the infrastructure to receive messages from the mobile app and setting up a timer which checks for a meeting occurrence at regular intervals. If you refer to the documentation of Class: Toybox::WatchUi::View there is a method called initialize() which acts as a constructor. Which means we can add a method called initialize in CalendarAlertsView class which can initialize the basic stuff like setting up the communication infrastructure and initializing the timer to scan upcoming meetings. So lets add this initialize() method to CalendarAlertsView.mc.
       var count=0;
       var meetingsArray= new [100] ;
       var timer;
       var app;

   function initialize() {
              // when a message is received over Bluetooth LE it would be received by the onMail method described below
              Comm.setMailboxListener( method(:onMail) );
              Comm.emptyMailbox();
              app=App.getApp();
              
              //this method is described below
              checkForUpcomingMeetings();
              timer = new Timer.Timer();
              //the method checkForUpcomingMeetings will be called at an interval of every 30 seconds
              timer.start( method(:checkForUpcomingMeetings), 1000*30, true );
       }


  function checkForUpcomingMeetings() {
          Sys.println("Checking for meetings");
       }

  function onMail(mailIter)
    {
         var  mail = mailIter.next();
        while( mail != null )
        {
             count++;
            
           //the variable 'mail' would contain a comma separated information about each meeting, create a MeetingAlert object out of it
            var meetingAlert = new MeetingAlert(mail);
            
           // store the meeting alerts for next 3 days stored in an in-memory array of MeetingAlerts
            meetingsArray[count]=meetingAlert;
            mail = mailIter.next();
        }
        Comm.emptyMailbox();
        return true;
    }
   
The method initialize() registers onMail() method as a mail box listener by calling Comm.setMailBoxListener. Mail box is a virtual place where messages sent over Bluetooth LE will arrive and on receipt of the same it invokes the listener method - in our case the onMail() method. Then we create a Timer which executes a method checkForUpcomingMeetings at a regular interval of 30 seconds. The Timer works in a similar way as setInterval method in Javascript or a TimerTask in Java. Right now the method checkForUpcomingMeetings just prints a single statement "Checking for meetings" on the console. However if you try to run this, your app wont start because you need to grant the necessary permission to the app for communicating over Bluetooth LE which can be done by adding following lines in your manifest.xml file.
        
            
        
 
If you run your application now it should show the watch app with a message "Upcoming Meetings - None in next 15 mins" and should print "Checking for upcoming meetings" every 30 seconds.
In next part of this tutorial we will setup our Android app to pass on the meeting information over Bluetooth LE to our watch widget.