apps on your wrist

285
@sarbogast @eloudsa #Devoxx #smartvoxx Apps On Your Wrist Sébastien Arbogast Said Eloudrhiri

Upload: sebastien-arbogast

Post on 21-Jan-2017

690 views

Category:

Technology


0 download

TRANSCRIPT

@sarbogast @eloudsa#Devoxx #smartvoxx

Apps On Your WristSébastien Arbogast

Said Eloudrhiri

#Devoxx #smartvoxx @sarbogast @eloudsa

• Who owns a smartwatch?

• Who is an Android developer?

• Who is an iOS developer?

• Who is a Pebble developer?

• Who is a Rolex developer?

• Who has already written a smartwatch app?

• Who is a member of the Night’s Watch?

Survey

#Devoxx #smartvoxx @sarbogast @eloudsa

Sébastien Arbogast@sarbogast

• Java developer for 10 years

• iOS developers for 5 years (developer of the first Devoxx schedule app)

• Pebble developer for 2 years

• Owner of TikTok Lunatik with iPod Nano

• VP of engineering for Take Eat Easy

#Devoxx #smartvoxx @sarbogast @eloudsa

Said Eloudrhiri @eloudsa

• Developer since 1992

• Agile Coach and trainer

• Devoxx4Kids helper (Sphero, MindStorms, CodeCombat)

• Side Projects: mobile development

• Husband and father of Nora, Rayane and Djenna

• No kitten but a dog

#Devoxx #smartvoxx @sarbogast @eloudsa

DisclaimerWe are not related to Google, Apple or Pebble.We are just curious developers sharing our experience.Materials used in this presentation remains the property of their owners.

Any questions?

#Devoxx #smartvoxx @sarbogast @eloudsa

Once upon a time …

#Devoxx #smartvoxx @sarbogast @eloudsa

Polex (1000 BC)

#Devoxx #smartvoxx @sarbogast @eloudsa

Pulsar P1 (70’s)

#Devoxx #smartvoxx @sarbogast @eloudsa

Casio Databank (80’s)

#Devoxx #smartvoxx @sarbogast @eloudsa

Linux wristwatch (90’s)

#Devoxx #smartvoxx @sarbogast @eloudsa

TikTok Lunatik (2011)

#Devoxx #smartvoxx @sarbogast @eloudsa

Pebble (2012)

#Devoxx #smartvoxx @sarbogast @eloudsa

Samsung Galaxy Gear (2013)

#Devoxx #smartvoxx @sarbogast @eloudsa

Moto 360 (2014)

#Devoxx #smartvoxx @sarbogast @eloudsa

Apple Watch (2015)

#Devoxx #smartvoxx @sarbogast @eloudsa

Why develop for smartwatches?

#Devoxx #smartvoxx @sarbogast @eloudsa

Glanceable

#Devoxx #smartvoxx @sarbogast @eloudsa

Sensors

#Devoxx #smartvoxx @sarbogast @eloudsa

Notification-driven

#Devoxx #smartvoxx @sarbogast @eloudsa

Small screen

#Devoxx #smartvoxx @sarbogast @eloudsa

Interactions

#Devoxx #smartvoxx @sarbogast @eloudsa

Personal use

#Devoxx #smartvoxx @sarbogast @eloudsa

Landscape

Apple Watch

Android Wear

Pebble Tizen

#Devoxx #smartvoxx @sarbogast @eloudsa

• Form factor: 38 mm and 42 mm (Square)

• Four kinds of applications: apps, notifications, glances, complications

• Design guidelines: personal communication, holistic design, lightweight interaction

Design constraints on Apple Watch

#Devoxx #smartvoxx @sarbogast @eloudsa

• Form factors

• Kinds of applications

• Design guidelines

Design constraints on Android Wear

#Devoxx #smartvoxx @sarbogast @eloudsa

Fragmentation: Welcome!

#Devoxx #smartvoxx @sarbogast @eloudsa

Square Round Round Chin

Design constraints on Android Wear

#Devoxx #smartvoxx @sarbogast @eloudsa

• Suggest: Context Stream

UI/UX Principles

The right information at the right time.

#Devoxx #smartvoxx @sarbogast @eloudsa

• Demand: Cue Cards

UI/UX Principles

No suggestions? Just ask!

#Devoxx #smartvoxx @sarbogast @eloudsa

• Contextually aware and smart

UI/UX Principles

#Devoxx #smartvoxx @sarbogast @eloudsa

• Cards

Applications: Notifications

#Devoxx #smartvoxx @sarbogast @eloudsa

• Bridged notifications: natively supported

Applications: Notifications

#Devoxx #smartvoxx @sarbogast @eloudsa

4

• Custom notifications

Applications: Notifications

#Devoxx #smartvoxx @sarbogast @eloudsa

Applications: Full-screen

#Devoxx #smartvoxx @sarbogast @eloudsa

Applications: Watch faces

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

• smartvoxx.com

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 1: Hello Devoxx!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 1: Hello Devoxx!

• Check the requirements

• Android Studio

• Android SDK Libraries

• Create the project (Mobile + Wear)

• Create Wear emulators (Square, Round, Round Chin)

• Change resources

• Run the application

#Devoxx #smartvoxx @sarbogast @eloudsa

• Mobile running Android 4.3 (API 18) or higher

• Watch running Android 5.0 (API 20) or higher

Requirements

#Devoxx #smartvoxx @sarbogast @eloudsa

Requirementsg.co/WearCheck

#Devoxx #smartvoxx @sarbogast @eloudsa

http://tools.android.com/download/studio/stable

Android Studio

#Devoxx #smartvoxx @sarbogast @eloudsa

Android SDK Manager

#Devoxx #smartvoxx @sarbogast @eloudsa

• Tools:

• Android SDK Tools

• Android SDK Platform-Tools

• Android SDK Build-Tools

• Android from API 18 (4.3.1) to API 22 (5.1.1) and higher

• SDK Platform

• Google APIs

• Android Wear Intel x86 System Image

• Google APIs Intel x86 Atom System Image

Required libs

#Devoxx #smartvoxx @sarbogast @eloudsa

• Extras:

• Android Support Repository

• Android Support Library

• Google Play Services

• Google Repository

Required libs

#Devoxx #smartvoxx @sarbogast @eloudsa

Android Studio or Eclipse?

• Android Development Tools (ADT)

• Andmore - Eclipse Android Tooling (Incubation Project)https://projects.eclipse.org/projects/tools.andmore

http://developer.android.com/tools/sdk/eclipse-adt.html

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Create the projet

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Two modules: mobile + wear

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 1: Development environment

#Devoxx #smartvoxx @sarbogast @eloudsa

Create wear emulators

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Emulator per form factor

#Devoxx #smartvoxx @sarbogast @eloudsa

• Run the Wear module:

• Choose an emulator:

Run on Wear Emulator

#Devoxx #smartvoxx @sarbogast @eloudsa

Square Round Round Chin

Step 1: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 1: Development environment

• XCode 7

• Swift or Objective-C

• WatchKit and WatchOS

• Either create iOS+Watchkit project from scratch

• Or add Watch extension to existing iOS app

• No standalone Watch app

• iOS + WatchOS simulator

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 1: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 2: Devoxx CFP API

• Schedules

• Slots

• BreakSlot

• TalkSlot

• Speakers

#Devoxx #smartvoxx @sarbogast @eloudsa

{ "links": [ { "href": "http://cfp.devoxx.be/api/conferences/DV15/schedules/monday/", "rel": "http://cfp.devoxx.be/api/profile/schedule", "title": "Monday, 9th November 2015 }, … ]}

Scheduleshttp://cfp.devoxx.be/api/conferences/DV15/schedules/

#Devoxx #smartvoxx @sarbogast @eloudsa

{"slots": [ { "roomId": "a_hall", "notAllocated": false, "fromTimeMillis": 1447052400000, "break": { "id": "reg", "nameEN": "Registration, Welcome and Breakfast", "nameFR": "Accueil", "room": { "id": "a_hall", "name": "Exhibition floor", "capacity": 1500, "setup": "special" } }, … } ]}

Slothttp://cfp.devoxx.be/api/conferences/DV15/schedules/monday/

#Devoxx #smartvoxx @sarbogast @eloudsa

API for Devoxx sessions

• http://cfp.devoxx.be/api

• http://cfp.devoxx.fr/api

• http://cfp.devoxx.co.uk/api

• http://cfp.devoxx.ma/api

• http://cfp.devoxx.pl/api

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 2: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 3: Get Schedules

…monday-Monday,9thNovember2015…tuesday-Tuesday,10thNovember2015…wednesday-Wednesday,11thNovember2015…thursday-Thursday,12thNovember2015…friday-Friday,13thNovember2015

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 3: Get Schedules

• Google Play Services and the Data Layer API:

• Node API, Message API

• The watch sends a request to the phone

• The phone accesses the network to fetch the schedules

• The phone logs the schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

Google Play Services

Google Play Services

#Devoxx #smartvoxx @sarbogast @eloudsa

Data Layer API

Google Play ServicesData Layer

Message API

Data API

Node API

#Devoxx #smartvoxx @sarbogast @eloudsa

Google API ClientDevice

Google Play Services

Your App

Google API Client

Google Play services library Message API

Data API

Node API

#Devoxx #smartvoxx @sarbogast @eloudsa

Node API

• Learn more about local or connected Nodes

• Display name

• Id to identify the node in the Android Wear network

• Nearby the local node

#Devoxx #smartvoxx @sarbogast @eloudsa

Message API

• One-way communication

• Message (Data Item) sent to the connected device

• path -> identifies the message

• payload -> small message payload

#Devoxx #smartvoxx @sarbogast @eloudsa

Data Item

Path Payload

/path/to/your/data Byte array max: 100 Kb

Asset (Binary Blob) can go beyond this limitation of 100Kb.Requires the Data API.

#Devoxx #smartvoxx @sarbogast @eloudsa

Watch

MessageApi

sendMessage()

Data Layer

Phone

WearableListenerService

onMessageReceived()

Data LayerBluetooth/Wi-Fi

MessageEvent

getData()getPath()

Data Item

Message API

#Devoxx #smartvoxx @sarbogast @eloudsa

Wi-Fi

Fallback solution when Bluetooth not available.

Unable to connect on remote servers.

#Devoxx #smartvoxx @sarbogast @eloudsa

Watch part

• Declare the Google API Client

• Connect-Disconnect to-from Google Play Services

• Define the message path

• Add a button listener

• Send the message to the phone to get the schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

Phone part• WearableListenerService: receive events from the Data Layer

• Process message path “/schedules” (onMessageReceived)

• Retrieves schedules with Retrofit (REST API Client)

• Logs schedules on the console

#Devoxx #smartvoxx @sarbogast @eloudsa

<Buttonandroid:id="@+id/getSchedules"style="?android:attr/buttonStyleSmall"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal" android:text="GetSchedules"/>

Layout: Add a button

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassScheduleActivityextendsActivity{

privateGoogleApiClientmApiClient;

@OverrideprotectedvoidonStart(){super.onStart();mApiClient=newGoogleApiClient.Builder(this) .addApi(Wearable.API).build();mApiClient.connect();}@OverrideprotectedvoidonStop(){if((mApiClient!=null)&&(mApiClient.isConnected())){ mApiClient.disconnect();}super.onStop();}

Activity: Google API Client

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassScheduleActivityextendsActivity{

privatefinalStringSCHEDULES_PATH="/schedules";

@OverrideprotectedvoidonCreate(BundlesavedInstanceState){

…stub.setOnLayoutInflatedListener(newWatchViewStub.OnLayoutInflatedListener(){ @OverridepublicvoidonLayoutInflated(WatchViewStubstub){ mTextView=(TextView)stub.findViewById(R.id.text); stub.findViewById(R.id.getSchedules).setOnClickListener(newView.OnClickListener(){ @OverridepublicvoidonClick(Viewv){sendMessage(SCHEDULES_PATH,"dummy"); }});}});

}

Activity: Button Listener

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassScheduleActivityextendsActivity{

privatevoidsendMessage(finalStringpath,finalStringmessage){ newThread(newRunnable(){@Overridepublicvoidrun(){//broadcastthemessagetoallconnecteddevices finalNodeApi.GetConnectedNodesResultnodes=Wearable.NodeApi.getConnectedNodes(mApiClient).await(); for(Nodenode:nodes.getNodes()){ Wearable.MessageApi.sendMessage(mApiClient,node.getId(),path,message.getBytes()).await(); }}}).start();}

Activity: Send Message

#Devoxx #smartvoxx @sarbogast @eloudsa

dependencies{compilefileTree(dir:'libs',include:['*.jar']) wearAppproject(':wear')compile'com.android.support:appcompat-v7:22.2.1'compile‘com.google.android.gms:play-services:8.1.0’//RestAPIClientcompile‘com.squareup.retrofit:retrofit:1.9.0’

}

REST API Client library: Retrofit build.gradle

#Devoxx #smartvoxx @sarbogast @eloudsa

packagenet.noratek.smartvoxxwear.rest.model;importjava.util.List;publicclassSchedules{privateList<Link>links;publicList<Link>getLinks(){returnlinks;}}

Model: Schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

packagenet.noratek.smartvoxxwear.rest.model; /***Createdbyeloudsaon06/09/15.*/publicclassLink{privateStringhref;privateStringrel;privateStringtitle;publicLink(Stringhref,Stringrel,Stringtitle){ this.href=href;this.rel=rel;this.title=title;}//GettersandSetters…}

Model: Link

#Devoxx #smartvoxx @sarbogast @eloudsa

packagenet.noratek.smartvoxxwear.rest.service; importnet.noratek.smartvoxxwear.rest.model.Schedules; importretrofit.Callback;importretrofit.http.GET;importretrofit.http.Path;/***Createdbyeloudsaon30/10/15.*/publicinterfaceDevoxxApi{@GET("/conferences/{conference}/schedules") voidgetSchedules(@Path("conference")Stringconference,Callback<Schedules>callback); }

REST Endpoint for Schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

WearableListenerService

• New class WearService extended from WearableListenerService

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear">

<uses-permissionandroid:name="android.permission.INTERNET"/><application…<!--AndroidWearService--><serviceandroid:name=".service.WearService"> <intent-filter><actionandroid:name="com.google.android.gms.wearable.BIND_LISTENER"/> </intent-filter></service></application></manifest>

Service: Adapt the manifest

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassWearServiceextendsWearableListenerService{

@OverridepublicvoidonMessageReceived(MessageEventmessageEvent){ //ProcessingtheincomingmessageStringpath=messageEvent.getPath();Stringdata=newString(messageEvent.getData()); if(path.equalsIgnoreCase(SCHEDULES_PATH)){ retrieveSchedules();return;}}

Process “/schedules”

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassWearServiceextendsWearableListenerService{ …//RetrieveschedulesfromDevoxxprivatevoidretrieveSchedules(){//retrievethescheduleslistfromtheserver Callbackcallback=newCallback(){@Overridepublicvoidsuccess(Objecto,Responseresponse){ //retrieveschedulefromRESTSchedulesscheduleList=(Schedules)o; if(scheduleList==null){Log.d(TAG,"Noschedules!");return;}List<Link>links=scheduleList.getLinks(); for(Linklink:links){Log.d(TAG,Utils.getLastPartUrl(link.getHref())+"-"+link.getTitle()); }}@Overridepublicvoidfailure(RetrofitErrorretrofitError){ Log.d(TAG,retrofitError.getMessage()); }};mMethods.getSchedules(mConferenceName,callback); }

Retrieve Schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

Run on Phone

• Build and deploy on Phone and Watch

• Forwarding ports (adb forward tcp:…):

• Link Phone to Wear Emulator

• On the Watch, tap on “GET SCHEDULES” button

• Check the log output of the phone

#Devoxx #smartvoxx @sarbogast @eloudsa

• Start the emulator

• Start a virtual device

Start Wear Emulator

#Devoxx #smartvoxx @sarbogast @eloudsa

Debugging with Emulator

USB

Bridge

adb -d forward tcp:5601 tcp:5601

#Devoxx #smartvoxx @sarbogast @eloudsa

• Select the top right menu

• Select “Pair with emulator”

Pairing the emulator

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

…monday-Monday,9thNovember2015…tuesday-Tuesday,10thNovember2015…wednesday-Wednesday,11thNovember2015…thursday-Thursday,12thNovember2015…friday-Friday,13thNovember2015

Step 3: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 3: Get Schedules

• If phone is connected, go through phone

• Otherwise access the network directly

• Transparent for the developer

• Access to the same network SDK as on the iPhone (NSURLSession)

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 3: Get Schedules

• Bypass App Transport Security

• Create Devoxx singleton to handle API client stuff in watch extension

• Initialize session configuration

• Initialize session

• Call the API

• Parse JSON into dictionaries

• Log dictionaries to the console

#Devoxx #smartvoxx @sarbogast @eloudsa

Bypass transport security

#Devoxx #smartvoxx @sarbogast @eloudsa

import WatchKit

class Devoxx: NSObject { static var sharedInstance = Devoxx() func loadSchedulesForConference(conference:String, callback: ([NSDictionary]) -> (Void)) { let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() configuration.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalCacheData let session = NSURLSession(configuration: configuration) guard let schedulesURL = NSURL(string: "http://cfp.devoxx.be/api/conferences/\(conference)/schedules/")! let task = session.dataTaskWithURL(schedulesURL) { (data: NSData?, response:NSURLResponse?, error:NSError?) -> Void in //Process data } task.resume() } }

Data singleton

#Devoxx #smartvoxx @sarbogast @eloudsa

guard let data = data else { print(error)

return } do {

//Parse data let schedulesDict = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) guard let schedulesArray = schedulesDict["links"] as? [NSDictionary] else {

print("No links array in parsed schedules") return } callback(schedulesArray) } catch let jsonError {

print(jsonError) }

API response processing

#Devoxx #smartvoxx @sarbogast @eloudsa

class ExtensionDelegate: NSObject, WKExtensionDelegate {

func applicationDidBecomeActive() { Devoxx.sharedInstance.loadSchedulesForConference("DV15") { (schedules:[NSDictionary]) -> (Void) in for schedule in schedules { print(schedule) } } }

}

Logging schedules to the console

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 3: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Show Schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Show Schedules

• The phone sends the schedules to the watch (Data API)

• The watch receives the schedules

• The watch displays the schedules on a list view

#Devoxx #smartvoxx @sarbogast @eloudsa

Data API

• Support one-way or two-way data communication

• Sending Binary Blog (Asset)

• Synchronise data between connected devices

• Synchronise data when connection is re-established

• Data caching

#Devoxx #smartvoxx @sarbogast @eloudsa

Phone

Data Layer

Watch

Data Layer

.create(“/path”)

PutDataMapRequest

.putInt(KEY, data)

DataMap

.asPutDataRequest()

PutDataRequest

.putDataItem()

Wearable.DataApi

Data Item (Shared)

.getInt()

DataMap

.getDataItem()

DataEvent

.fromDataItem(DataItem)

DataMapItem

onDataChanged()

DataApi.DataListenerBluetooth/Wi-Fi

Data API

#Devoxx #smartvoxx @sarbogast @eloudsa

@OverrideprotectedvoidonResume(){super.onResume();//RetrievethelistofschedulessendMessage(SCHEDULES_PATH,"getlistofschedules");}

Display schedules

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassWearServiceextendsWearableListenerService{

//sendSchedulestothewatchprivatevoidsendSchedules(List<Link>schedules){ finalPutDataMapRequestputDataMapRequest=PutDataMapRequest.create(SCHEDULES_PATH); ArrayList<DataMap>schedulesDataMap=newArrayList<>(); //processeachschedulefor(Linkschedule:schedules){finalDataMapscheduleDataMap=newDataMap(); //processandpushschedule'sdata//WeneedtoaddatimestamptoforceaonDataChangedeventontheremotedevice. scheduleDataMap.putString("timestamp",newDate().toString());

scheduleDataMap.putString("day",Utils.getLastPartUrl(schedule.getHref())); scheduleDataMap.putString("title",schedule.getTitle()); schedulesDataMap.add(scheduleDataMap); }//storethelistinthedatamaptosendittothewatch putDataMapRequest.getDataMap().putDataMapArrayList("/list",schedulesDataMap); //sendthelistif(mApiClient.isConnected()){Wearable.DataApi.putDataItem(mApiClient,putDataMapRequest.asPutDataRequest()); }}

Send Schedules

1

2

3

5

6

4

#Devoxx #smartvoxx @sarbogast @eloudsa

public class ScheduleActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {

@Override protected void onStart() { super.onStart(); mApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .build(); mApiClient.connect(); }

}

Add Listeners

#Devoxx #smartvoxx @sarbogast @eloudsa

public class ScheduleActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {

@Override public void onConnected(Bundle bundle) { Wearable.DataApi.addListener(mApiClient, this);}

}

Add Listeners

#Devoxx #smartvoxx @sarbogast @eloudsa

@Override public void onDataChanged(DataEventBuffer dataEventBuffer) { for (DataEvent event : dataEventBuffer) { // Check if we have received our schedules if (event.getType() == DataEvent.TYPE_CHANGED && event.getDataItem().getUri().getPath().startsWith(SCHEDULES_PATH)) { SchedulesListWrapper schedulesListWrapper = new SchedulesListWrapper(); final List<Schedule> schedulesList = schedulesListWrapper.getSchedulesList(event); runOnUiThread(new Runnable() { @Override public void run() { // hide the progress bar findViewById(R.id.progressBar).setVisibility(View.GONE); listViewAdapter.refresh(schedulesList); } }); return; } } }

1

2

3

4

Add Listeners

#Devoxx #smartvoxx @sarbogast @eloudsa

Layout: Rect

#Devoxx #smartvoxx @sarbogast @eloudsa

@OverrideprotectedvoidonCreate(BundlesavedInstanceState){ …//ListviewcomponentlistView=(WearableListView)findViewById(R.id.wearable_list); //AssigntheadapterlistViewAdapter=newListViewAdapter(ScheduleActivity.this,newArrayList<Schedule>());listView.setAdapter(listViewAdapter); …}

ListViewAdapter

#Devoxx #smartvoxx @sarbogast @eloudsa

WearableListView

#Devoxx #smartvoxx @sarbogast @eloudsa

//InnerclassprovidingtheWearableListview'sadapter publicclassListViewAdapterextendsWearableListView.Adapter{ …//Createnewviewsforlistitems//(invokedbytheWearableListView'slayoutmanager) @OverridepublicWearableListView.ViewHolderonCreateViewHolder(ViewGroupparent, intviewType){ //Inflateourcustomlayoutforlistitems returnnewItemViewHolder(newSettingsItemView(mContext));}…

Animation

#Devoxx #smartvoxx @sarbogast @eloudsa

publicfinalclassSettingsItemViewextendsFrameLayoutimplementsWearableListView.OnCenterProximityListener{privateTextViewdescription;publicSettingsItemView(Contextcontext){ super(context);View.inflate(context,R.layout.schedule_row_activity,this); description=(TextView)findViewById(R.id.description); }@OverridepublicvoidonCenterPosition(booleanb){description.animate().scaleX(1f).scaleY(1f).alpha(1); }@OverridepublicvoidonNonCenterPosition(booleanb){description.animate().scaleX(0.8f).scaleY(0.8f).alpha(0.6f); }}

Animation

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" …><TextViewandroid:id="@+id/title"…/><RelativeLayoutandroid:layout_width="fill_parent" android:layout_height="fill_parent"> <android.support.wearable.view.WearableListView android:id="@+id/wearable_list" …></android.support.wearable.view.WearableListView> <ProgressBarandroid:id="@+id/progressBar" …/></RelativeLayout></LinearLayout>

Layout: schedule_rect_activity1

2

3

4

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><mergexmlns:android="http://schemas.android.com/apk/res/android"> <TextViewandroid:id="@+id/description"…/></merge>

Layout: schedule_row_activity

#Devoxx #smartvoxx @sarbogast @eloudsa

Run the app

• Build and deploy on Phone and Watch

• Forwarding ports (adb forward tcp:…):

• Link Phone to Wear Emulator

• Data are retrieved and displayed from Phone

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Show Schedules

• Model classes to store data (Model)

• Storyboard to layout screens (View)

• InterfaceControllers to configure and react (Controllers)

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Show Schedules

• Create Schedule class: initializer and overridden description

• Replace dictionary by class in callback

• Remove label and add table to layout

• Specify identifier for row controller

• Create ScheduleRowController class

• Link ScheduleRowController in storyboard

• Label outlet in ScheduleRowController

• Table outlet in interface controller

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Show Schedules

• Call Devoxx loading method in willActivate and initialize table

• Remove Devoxx call from ExtensionDelegate

• RUN!

#Devoxx #smartvoxx @sarbogast @eloudsa

class Schedule: NSObject { var title:String? var href:NSURL? init(fromDictionary dictionary:NSDictionary){ guard let title = dictionary["title"] as? String else { print("Cannot find title") return } guard let href = dictionary["href"] as? String else { print("Cannot find href") return } self.title = title self.href = NSURL(string: href) } override var description:String { return self.title! } }

Schedule model class

#Devoxx #smartvoxx @sarbogast @eloudsa

func loadSchedulesForConference(conference:String, callback: ([Schedule]) -> (Void)) { let task = session.dataTaskWithURL(schedulesURL) { (data: NSData?, response:NSURLResponse?, error:NSError?) -> Void in do { let schedulesDict=try NSJSONSerialization.JSONObjectWithData(data, options:.AllowFragments) guard let schedulesArray = schedulesDict["links"] as? [NSDictionary] else { print("No links array in parsed schedules") return } var schedules = [Schedule]() for scheduleDict in schedulesArray { schedules.append(Schedule(fromDictionary: scheduleDict)) } callback(schedules) } catch let jsonError { print(jsonError) } } task.resume() }

Replace dictionary by model

#Devoxx #smartvoxx @sarbogast @eloudsa

Add WKInterfaceTable to view

#Devoxx #smartvoxx @sarbogast @eloudsa

Identify row controller

#Devoxx #smartvoxx @sarbogast @eloudsa

import WatchKit

class ScheduleRowController: NSObject { }

ScheduleRowController class

#Devoxx #smartvoxx @sarbogast @eloudsa

Label outlet in row controller

#Devoxx #smartvoxx @sarbogast @eloudsa

Table outlet in interface controller

#Devoxx #smartvoxx @sarbogast @eloudsa

override func willActivate() { super.willActivate() Devoxx.sharedInstance.loadSchedulesForConference("DV15") { (schedules:[Schedule]) -> (Void) in self.table.setNumberOfRows(schedules.count, withRowType: "Schedule") for (index, schedule) in schedules.enumerate() { guard let scheduleRowController = self.table.rowControllerAtIndex(index) as? ScheduleRowController else { print("Error in table configuration") return } scheduleRowController.titleLabel.setText(schedule.title) } } }

Call Devoxx API and init table

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 4: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 5: Select a Schedule

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 5: Select a Schedule

• Store Schedule’s data on Tag

• Add ClickListener on WearableListView

• Attach the listener

• Retrieve Schedule’s data from Tag

• Display the selected item

#Devoxx #smartvoxx @sarbogast @eloudsa

@OverridepublicvoidonBindViewHolder(WearableListView.ViewHolderholder,intposition){ //retrievethetextviewItemViewHolderitemHolder=(ItemViewHolder)holder; TextViewview=itemHolder.textView;//retrieve,transformanddisplaytheschedule'sday Scheduleschedule=mDataset.get(position); StringscheduleDay=schedule.getTitle().replace(",","\n"); view.setText(scheduleDay);//replacelistitem'smetadataholder.itemView.setTag(schedule);}

Store Schedule’s data1

2

#Devoxx #smartvoxx @sarbogast @eloudsa

public class ScheduleActivity extends Activity implements WearableListView.ClickListener, GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {

@Override public void onClick(WearableListView.ViewHolder viewHolder) { }

}

Add ClickListener

#Devoxx #smartvoxx @sarbogast @eloudsa

@Override protected void onCreate(Bundle savedInstanceState) {

… // Assign the adapter listViewAdapter = new ListViewAdapter(ScheduleActivity.this, new ArrayList<Schedule>()); listView.setAdapter(listViewAdapter); // Set the click listener listView.setClickListener(ScheduleActivity.this);

}

Attach the ClickListener

#Devoxx #smartvoxx @sarbogast @eloudsa

@Override public void onClick(WearableListView.ViewHolder viewHolder) { Schedule schedule = (Schedule) viewHolder.itemView.getTag(); if (schedule == null) { return; } Toast.makeText(ScheduleActivity.this, "You tap on: " + schedule.getDay(), Toast.LENGTH_SHORT).show();}

Retrieve and display schedule

1

2

#Devoxx #smartvoxx @sarbogast @eloudsa

Run the app

• Build and deploy on the Watch

• Forwarding ports (adb forward tcp:…):

• Link Phone to Wear Emulator

• Tap on a schedule

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 5: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 5: Select a Schedule• Add title to interface controller

• New interface controller in storyboard

• Push segue from first to second interface controller

• Create ScheduleInterfaceController class

• Link it to storyboard

• Add schedule member variable

• Override contextForSegueWithIdentifier

• Catch context in ScheduleInterfaceController

• Set title in willActivate

#Devoxx #smartvoxx @sarbogast @eloudsa

Add ScheduleInterfaceController

#Devoxx #smartvoxx @sarbogast @eloudsa

Give identifier to push segue

#Devoxx #smartvoxx @sarbogast @eloudsa

class InterfaceController: WKInterfaceController { @IBOutlet var table: WKInterfaceTable! var schedules:[Schedule]? […] override func contextForSegueWithIdentifier(segueIdentifier: String, inTable table: WKInterfaceTable, rowIndex: Int) -> AnyObject? { return self.schedules![rowIndex] } }

contextForSegue

#Devoxx #smartvoxx @sarbogast @eloudsa

class ScheduleInterfaceController: WKInterfaceController { var schedule:Schedule?

override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) if let schedule = context as? Schedule { self.schedule = schedule } }

override func willActivate() { super.willActivate() if let schedule = self.schedule, title = schedule.title { self.setTitle(title.componentsSeparatedByString(",")[0]) } } }

Catch context

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 5: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 6: Get Slots

#Devoxx #smartvoxx @sarbogast @eloudsa

Retrieve Slots

Get back on the Network.

Seriously???

#Devoxx #smartvoxx @sarbogast @eloudsa

Phone

Data Layer

Watch

Data Layer

.create(“/path”)

PutDataMapRequest

.putInt(KEY, data)

DataMap

.asPutDataRequest()

PutDataRequest

.putDataItem()

Wearable.DataApi

Data Item (Shared)

.getInt()

DataMap

.getDataItem()

DataEvent

.fromDataItem(DataItem)

DataMapItem

onDataChanged()

DataApi.DataListener

Data API

#Devoxx #smartvoxx @sarbogast @eloudsa

Wear

Data Item (Cache)

.getInt()

DataMap

.getDataIetms()

DataApi

wear://path_to_data

12

Fetch from local cache

MessageApi

sendMessage()

3

4

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassWearServiceextendsWearableListenerService{

//sendSchedulestothewatchprivatevoidsendSchedules(List<Link>schedules){ finalPutDataMapRequestputDataMapRequest=PutDataMapRequest.create(SCHEDULES_PATH); ArrayList<DataMap>schedulesDataMap=newArrayList<>(); //processeachschedulefor(Linkschedule:schedules){finalDataMapscheduleDataMap=newDataMap(); //processandpushschedule'sdatascheduleDataMap.putString("day",Utils.getLastPartUrl(schedule.getHref())); scheduleDataMap.putString("title",schedule.getTitle()); schedulesDataMap.add(scheduleDataMap); }//storethelistinthedatamaptosendittothewatch putDataMapRequest.getDataMap().putDataMapArrayList("/list",schedulesDataMap); //sendthelistif(mApiClient.isConnected()){Wearable.DataApi.putDataItem(mApiClient,putDataMapRequest.asPutDataRequest()); }}

Changes on Phone: No timestamp

1

#Devoxx #smartvoxx @sarbogast @eloudsa

@OverrideprotectedvoidonResume(){super.onResume();//Retrieveanddisplaythelistofschedules getSchedules(SCHEDULES_PATH);}

Display data items

#Devoxx #smartvoxx @sarbogast @eloudsa

private void getSchedules(final String pathToContent) { Uri uri = new Uri.Builder() .scheme(PutDataRequest.WEAR_URI_SCHEME) .path(pathToContent) .build(); Wearable.DataApi.getDataItems(mApiClient, uri) .setResultCallback( new ResultCallback<DataItemBuffer>() { @Override public void onResult(DataItemBuffer dataItems) { if (dataItems.getCount() == 0) { // refresh the list of schedules from Mobile sendMessage(SCHEDULES_PATH, "get list of schedules"); return; } … // retrieve and display the schedule from the cache SchedulesListWrapper schedulesListWrapper = new SchedulesListWrapper(); final List<Schedule> schedulesList = schedulesListWrapper.getSchedulesList(dataMap); runOnUiThread(new Runnable() { @Override public void run() { // hide the progress bar findViewById(R.id.progressBar).setVisibility(View.GONE); listViewAdapter.refresh(schedulesList); } }); } } ); }

Retrieve data: mobile or cache?1

2

3

4

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 6: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 6: Get and Cache Schedules

• Create activity indicator animation

• Link with CoreData framework

• Create object model

• Create DevoxxCache class

• Setup Core Data stack in DevoxxCache

• Model Schedule and Conference in object model

• Delete Schedule Model class

• Generate NSManagedObject subclasses

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 6: Get and Cache Schedules

• Add empty getSchedules() method to DevoxxCache

• Add empty saveSchedules() method to DevoxxCache

• Modify loadSchedules in Devoxx class

#Devoxx #smartvoxx @sarbogast @eloudsa

Create activity indicator animation

https://github.com/mikeswanson/JBWatchActivityIndicator

• Add image to interface controller

• Scale mode: center

• Height and width relative to container

• Hidden

• activityIndicator outlet in interface controller

#Devoxx #smartvoxx @sarbogast @eloudsa

class InterfaceController: WKInterfaceController { @IBOutlet var activityIndicator: WKInterfaceImage! var schedules:[Schedule]?

override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) self.activityIndicator.setImageNamed("Activity") }

override func willActivate() { super.willActivate() self.activityIndicator.setHidden(false) self.activityIndicator.startAnimating() Devoxx.sharedInstance.loadSchedulesForConference("DV15") { (schedules:[Schedule]) -> (Void) in self.schedules = schedules […] self.activityIndicator.setHidden(true) } } }

Activity indicator

#Devoxx #smartvoxx @sarbogast @eloudsa

Link with CoreData framework

#Devoxx #smartvoxx @sarbogast @eloudsa

Create object model

#Devoxx #smartvoxx @sarbogast @eloudsa

import CoreData

class DevoxxCache: NSObject { lazy var applicationDocumentsDirectory: NSURL = { let urls = NSFileManager.defaultManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask) return urls[urls.count - 1] }() lazy var managedObjectModel: NSManagedObjectModel = { let modelURL = NSBundle.mainBundle().URLForResource("Smartvoxx", withExtension: "momd")! return NSManagedObjectModel(contentsOfURL: modelURL)! }()

}

Set up Core Data stack

#Devoxx #smartvoxx @sarbogast @eloudsa

import CoreData

class DevoxxCache: NSObject { […] lazy var persistentStoreCoordinator: NSPersistentStoreCoordinator = { let coordinator = NSPersistentStoreCoordinator(managedObjectModel: self.managedObjectModel) let url = self.applicationDocumentsDirectory.URLByAppendingPathComponent("Smartvoxx.sqlite") do { print(url) try coordinator.addPersistentStoreWithType(NSSQLiteStoreType, configuration: nil, URL: url, options: nil) } catch { print(error) } return coordinator }() }

Set up Core Data stack

#Devoxx #smartvoxx @sarbogast @eloudsa

import CoreData

class DevoxxCache: NSObject { […] lazy var mainObjectContext: NSManagedObjectContext = { var managedObjectContext = NSManagedObjectContext(concurrencyType: .MainQueueConcurrencyType) managedObjectContext.persistentStoreCoordinator = self.persistentStoreCoordinator return managedObjectContext }() lazy var privateObjectContext: NSManagedObjectContext = { var privateContext = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType) privateContext.parentContext = self.mainObjectContext return privateContext }() }

Set up Core Data stack

#Devoxx #smartvoxx @sarbogast @eloudsa

import CoreData

class DevoxxCache: NSObject { […] override init() {} func saveContext(context: NSManagedObjectContext) { do { try context.save() if let parentContext = context.parentContext { try parentContext.save() } } catch { print(error) abort() } }}

Set up Core Data stack

#Devoxx #smartvoxx @sarbogast @eloudsa

Model Conference and Schedule

#Devoxx #smartvoxx @sarbogast @eloudsa

Model Conference and Schedule

#Devoxx #smartvoxx @sarbogast @eloudsa

Generate NSManagedObject subs

#Devoxx #smartvoxx @sarbogast @eloudsa

func loadSchedulesForConference(conference:String, callback: ([Schedule]) -> (Void)) { let schedules = cache.getSchedules() if schedules.count > 0 { dispatch_async(dispatch_get_main_queue(), { () -> Void in callback(schedules) }) } let schedulesURL = NSURL(string: "http://cfp.devoxx.be/api/conferences/\(conference)/schedules/")! let task = session.dataTaskWithURL(schedulesURL) { (data: NSData?, response:NSURLResponse?, error:NSError?) -> Void in guard let data = data else { print(error) return } self.cache.saveSchedulesFromData(data) dispatch_async(dispatch_get_main_queue(), { () -> Void in callback(self.cache.getSchedules()) }) } task.resume() }

In Devoxx…

#Devoxx #smartvoxx @sarbogast @eloudsa

func getSchedules() -> [Schedule] { return self.getSchedules(fromContext: self.mainObjectContext) } private func getSchedules(fromContext context: NSManagedObjectContext) -> [Schedule] { var schedules = [Schedule]() context.performBlockAndWait { () -> Void in let request = NSFetchRequest(entityName: "Conference") request.predicate = NSPredicate(format: "eventCode=%@", "DV15") do { let results = try context.executeFetchRequest(request) if results.count > 0 { guard let devoxx15 = results[0] as? Conference, scheduleSet = devoxx15.schedules, scheduleArray = scheduleSet.array as? [Schedule] else { schedules = [Schedule]() return } schedules = scheduleArray } } catch let error as NSError { print(error) } } return schedules }

Back in DevoxxCache…

#Devoxx #smartvoxx @sarbogast @eloudsa

private func saveSchedulesFromData(data: NSData, inContext context: NSManagedObjectContext) { context.performBlockAndWait { () -> Void in if data.length > 0 { do { let schedulesDict = try NSJSONSerialization.JSONObjectWithData(data, options: .AllowFragments) if let schedulesDict = schedulesDict as? NSDictionary, schedulesArray = schedulesDict["links"] as? NSArray { guard let devoxx15 = self.getOrCreateDevoxx15(inContext: context) else { print("Could not retrieve Devoxx15 conference") return } var schedules = [Schedule]() for scheduleDict in schedulesArray { if let scheduleDict = scheduleDict as? NSDictionary { guard let schedule = self.getOrCreateScheduleForHref(scheduleDict["href"] as! String, inContext: context) else { print("Could not retrieve or create schedule") return } schedule.title = scheduleDict["title"] as? String schedule.href = scheduleDict["href"] as? String schedule.conference = devoxx15 self.saveContext(context) schedules.append(schedule) } } devoxx15.schedules = NSOrderedSet(array: schedules) self.saveContext(context) } } catch let jsonError as NSError { print(jsonError) } } } }

Back in DevoxxCache…

#Devoxx #smartvoxx @sarbogast @eloudsa

private func getOrCreateDevoxx15(inContext context: NSManagedObjectContext) -> Conference? { let request = NSFetchRequest(entityName: "Conference") request.predicate = NSPredicate(format: "eventCode=%@", "DV15") var devoxx15: Conference? context.performBlockAndWait { () -> Void in do { let results = try context.executeFetchRequest(request) if results.count > 0 { devoxx15 = results[0] as? Conference } else { devoxx15 = NSEntityDescription.insertNewObjectForEntityForName("Conference", inManagedObjectContext: context) as? Conference devoxx15!.eventCode = "DV15" devoxx15!.label = "Devoxx 2015" devoxx15!.localisation = "Antwerp, Belgium" self.saveContext(context) } } catch let error as NSError { print(error) } } return devoxx15 }

Back in DevoxxCache…

#Devoxx #smartvoxx @sarbogast @eloudsa

private func getOrCreateScheduleForHref(href: String, inContext context: NSManagedObjectContext) -> Schedule? { var schedule: Schedule? context.performBlockAndWait { () -> Void in let request = NSFetchRequest(entityName: "Schedule") request.predicate = NSPredicate(format: "href=%@", href) do { let results = try context.executeFetchRequest(request) if results.count > 0 { schedule = results[0] as? Schedule } else { schedule = NSEntityDescription.insertNewObjectForEntityForName("Schedule", inManagedObjectContext: context) as? Schedule schedule!.href = href self.saveContext(context) } } catch let error as NSError { print(error) } } return schedule }

Back in DevoxxCache…

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 6: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: Get Talk

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: Get Talk

• Create the Activity

• Layouts: GridView, Card, Custom

• GridViewPagerAdapter

• Create the Fragments

• EventBus

• Retrieve Talks and Speakers from Data Api

• Requests sent through the Message Api

• Bonus: Follow on Twitter (Confirmation Activity)

#Devoxx #smartvoxx @sarbogast @eloudsa

GridViewPager

Row 1: • Column 1: Talk information • Column 2: Summary

Row 2: • Column 1: Speaker 1 • Column 2: Speaker 2 • … • Column n: Speaker n

#Devoxx #smartvoxx @sarbogast @eloudsa

Fragment Fragment

Fragment Fragment

• TalkFragment • TalkSummaryFragment • TalkSpeakerFragment

FragmentGridPagerAdapter

TalkActivity

#Devoxx #smartvoxx @sarbogast @eloudsa

• Define the layout

• Create the fragments

• Create the Adapter

• Link the Adapter

Create the GridViewPager

#Devoxx #smartvoxx @sarbogast @eloudsa

<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/black"><android.support.wearable.view.GridViewPagerxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pager"android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="false"/><android.support.wearable.view.DotsPageIndicator android:id="@+id/page_indicator"android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|top" app:dotFadeOutDelay="10000"></android.support.wearable.view.DotsPageIndicator> </FrameLayout>

Layoutres/layout/talk_activity.xml

#Devoxx #smartvoxx @sarbogast @eloudsa

Talk detail: Custom layout

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><android.support.wearable.view.WatchViewStub xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/watch_talk_stub"android:layout_width="match_parent"android:layout_height="match_parent"app:rectLayout="@layout/talk_rect_fragment" app:roundLayout="@layout/talk_round_fragment" tools:context=".TalkActivity"tools:deviceIds="wear"></android.support.wearable.view.WatchViewStub>

Layoutres/layout/talk_fragment.xml

#Devoxx #smartvoxx @sarbogast @eloudsa

<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"android:layout_height="fill_parent"android:paddingTop="15dp"><TextViewandroid:id="@+id/title"android:layout_width="fill_parent" ….

Layoutres/layout/talk_rect_fragment.xml

#Devoxx #smartvoxx @sarbogast @eloudsa

<android.support.wearable.view.BoxInsetLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="match_parent"android:padding="15dp"><RelativeLayoutandroid:layout_width="match_parent" android:layout_height="match_parent"> <TextViewandroid:id="@+id/title"

Layoutres/layout/talk_round_fragment.xml

#Devoxx #smartvoxx @sarbogast @eloudsa

Summary: Card

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><android.support.wearable.view.CardScrollViewxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/card_scroll_view"android:layout_width="match_parent"android:layout_height="match_parent"><android.support.wearable.view.CardFrameandroid:id="@+id/card_frame"…><LinearLayoutandroid:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"><TextViewandroid:id="@+id/title"

Summary: Layout

#Devoxx #smartvoxx @sarbogast @eloudsa

Summary: Expand content

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassTalkSummaryFragmentextendsFragment{…description.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){if(mTalkSummary==null){return;}if(mEllipsize){description.setText(mTalkSummary);}else{description.setText(StringUtils.abbreviate(mTalkSummary,ELLIPSIS_SIZE)); }

mEllipsize=!mEllipsize;//ForcetheCardScrollViewtoresettoitsinitialposition mainView.findViewById(R.id.card_scroll_view).setScrollX(0); mainView.findViewById(R.id.card_scroll_view).setScrollY(0); }});

Summary: Expand content

#Devoxx #smartvoxx @sarbogast @eloudsa

TalkActivity

EventsFragments

Events

#Devoxx #smartvoxx @sarbogast @eloudsa

EventBus (BusWear)

#Devoxx #smartvoxx @sarbogast @eloudsa

EventBus (BusWear)

#Devoxx #smartvoxx @sarbogast @eloudsa

dependencies{compilefileTree(dir:'libs',include:['*.jar']) compile'com.google.android.support:wearable:1.3.0' compile'com.google.android.gms:play-services-wearable:8.1.0' //EventBus:BusWearcompile'pl.tajchert:buswear:0.9.5'…}

EventBusbuild.grade

#Devoxx #smartvoxx @sarbogast @eloudsa

EventBus.getDefault().postLocal(newTalkEvent(mTalk));

EventBus - Sending events

#Devoxx #smartvoxx @sarbogast @eloudsa

publicvoidonEvent(AddFavoriteEventaddFavoritesEvent){

}

EventBus - Receive eventsRequired to register/unregister to the bus

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassTalkActivityextendsActivity…{

@OverrideprotectedvoidonStart(){…EventBus.getDefault().register(this);}@OverrideprotectedvoidonStop(){EventBus.getDefault().unregister(this);…}

EventBus - (Un)Register

#Devoxx #smartvoxx @sarbogast @eloudsa

Twitter: Confirmation animation

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear">

…<activityandroid:name="android.support.wearable.activity.ConfirmationActivity"/>

Confirmation animationUpdate the manifest of the watch

#Devoxx #smartvoxx @sarbogast @eloudsa

@OverridepublicViewonCreateView(LayoutInflaterinflater,ViewGroupcontainer, BundlesavedInstanceState){

mainView.findViewById(R.id.twitterIcon).setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){startConfirmationActivity(ConfirmationActivity.OPEN_ON_PHONE_ANIMATION,getString(R.string.confirmation_open_on_phone));EventBus.getDefault().postLocal(newConfirmationEvent(TWITTER_PATH,(String)mainView.findViewById(R.id.twitterIcon).getTag())); }});

Confirmation animation (Listener)

1

2

#Devoxx #smartvoxx @sarbogast @eloudsa

privatevoidstartConfirmationActivity(intanimationType,Stringmessage){IntentconfirmationActivity=newIntent(getActivity(),ConfirmationActivity.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_ANIMATION) .putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,animationType).putExtra(ConfirmationActivity.EXTRA_MESSAGE,message); getActivity().startActivity(confirmationActivity);

}

Animation type

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassWearServiceextendsWearableListenerService{…privatevoidfollowOnTwitter(StringinputData){ StringtwitterName=inputData==null?"":inputData.trim().toLowerCase(); twitterName=twitterName.replaceFirst("@",""); if(twitterName.isEmpty()){return;}Intentintent=newIntent(Intent.ACTION_VIEW,Uri.parse("https://twitter.com/"+twitterName)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); this.startActivity(intent);}

}

Open Twitter on Phone

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: Square GridViewPagerCardCustom

Custom CustomWearableListView

Step 7: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: RoundCardCustom

Custom CustomWearableListView

GridViewPager

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: Get, Cache and Show Talk

• Time travel

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 7: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Favorites

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Set Favorites• Retrieve, add, remove favorites on the calendar

• Add CalendarHelper on the phone

• Favorite messages sent over DataApi (retrieved, added, removed)

• Add reminders using AlarmManager

• Manage a manual delete from the Calendar with a synchronisation of the SlotsActivity or TalkActivity

• Send a message when required (retrieve) or when the favorite has changed (add or remove)

• Use the EventBus to synchronise the components (Activity, Fragments)

• ConfirmationActivity on favorite’s action

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: FavoritesPhone Watch

12

3

4eventId

removeread

add

Calendar

removeread

add

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: RemindersPhone

1

Calendaradd

AlarmManager

2

10 minutes before

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Alarm ServicePhone

1

Calendaradd

AlarmManager

2

10 minutes before

AlarmService

talkId, title, eventId, schedule, …

3

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Wake-upPhone

AlarmManager

AlarmService

talkId, title, eventId, schedule, …1

AlarmReceiver

talkId, title, eventId, schedule, …

.broadcast()

2

#Devoxx #smartvoxx @sarbogast @eloudsa

<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear"><!--Add,Readandremovefavoritesonthecalendar--> <uses-permissionandroid:name="android.permission.READ_CALENDAR"/> <uses-permissionandroid:name="android.permission.WRITE_CALENDAR"/>

Calendar: Add permissionsUpdate the manifest of the phone

#Devoxx #smartvoxx @sarbogast @eloudsa

<application>…

<serviceandroid:name=".alarm.AlarmService"/><receiverandroid:name=".alarm.AlarmReceiver"><intent-filter><actionandroid:name="net.noratek.smartvoxxwear.AlarmService.BROADCAST"/> </intent-filter></receiver>

</application

Alarm: Service, ReceiverUpdate the manifest of the phone

#Devoxx #smartvoxx @sarbogast @eloudsa

Add: Tap on favorite icon

#Devoxx #smartvoxx @sarbogast @eloudsa

Mobile: event on Calendar

• Title

• Room

• Summary

• Schedule

#Devoxx #smartvoxx @sarbogast @eloudsa

Watch: Confirmation received

#Devoxx #smartvoxx @sarbogast @eloudsa

Remove: Tap on favorite icon

#Devoxx #smartvoxx @sarbogast @eloudsa

Event removed from Calendar

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Stop 8: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Managing Favorites

• Add a force touch menu to SlotController

• Handle menu actions

• Add scheduleNotifications() method to talk to the phone

• Import WatchConnectivity framework

• Start WatchConnectivity session

• Receive messages in AppDelegate on iPhone

#Devoxx #smartvoxx @sarbogast @eloudsa

class SlotController: WKInterfaceController { override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) self.updateMenu() }

func updateMenu() { self.clearAllMenuItems() if let talkSlot = self.slot as? TalkSlot { if let favorite = talkSlot.favorite?.boolValue where favorite { self.addMenuItemWithImageNamed("FavoriteOffMenu", title: NSLocalizedString("Remove from Favorites", comment: ""), action: "favoriteMenuSelected") self.favoriteImage.setImageNamed("FavoriteOn") } else { self.addMenuItemWithImageNamed("FavoriteOnMenu", title: NSLocalizedString("Add to Favorites", comment: ""), action: "favoriteMenuSelected") self.favoriteImage.setImageNamed("FavoriteOff") } self.addMenuItemWithItemIcon(WKMenuItemIcon.Decline, title: NSLocalizedString("Cancel", comment: ""), action: "cancelMenuSelected") } } }

Add force touch menu

#Devoxx #smartvoxx @sarbogast @eloudsa

class SlotController: WKInterfaceController { […]

@IBAction func favoriteMenuSelected() { if let talkSlot = self.slot as? TalkSlot { DataController.sharedInstance.swapFavoriteStatusForTalkSlot(talkSlot, callback: { (talkSlot:TalkSlot) -> Void in self.slot = talkSlot self.updateMenu() }) } } @IBAction func cancelMenuSelected() {} }

Handle menu actions

#Devoxx #smartvoxx @sarbogast @eloudsa

class SlotController: WKInterfaceController { […]

@IBAction func favoriteMenuSelected() { if let talkSlot = self.slot as? TalkSlot { DataController.sharedInstance.swapFavoriteStatusForTalkSlot(talkSlot, callback: { (talkSlot:TalkSlot) -> Void in self.slot = talkSlot self.updateMenu()

self.scheduleNotification() }) } } @IBAction func cancelMenuSelected() {} }

Handle menu actions

#Devoxx #smartvoxx @sarbogast @eloudsa

import WatchConnectivity

class SlotController: WKInterfaceController, WCSessionDelegate { var session:WCSession?

override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) […] self.updateMenu() startSession() } private func startSession() { if WCSession.isSupported() { session = WCSession.defaultSession() session?.delegate = self session?.activateSession() } } }

Start WCSession

#Devoxx #smartvoxx @sarbogast @eloudsa

private func scheduleNotification() { if let talkSlot = self.slot as? TalkSlot { let talkSlotMessage = [ "title":talkSlot.title!, "room":talkSlot.roomName!, "talkId":talkSlot.talkId!, "track":talkSlot.track!.name!, "favorite":talkSlot.favorite!, "fromTimeMillis":talkSlot.fromTimeMillis!, "fromTime":talkSlot.fromTime!, "toTime":talkSlot.toTime! ] if WCSession.isSupported() { if let session = self.session where session.reachable { session.sendMessage(["talkSlot" : talkSlotMessage as NSDictionary], replyHandler: nil, errorHandler: { (error:NSError) -> Void in print(error) }) } else { session?.transferUserInfo(["talkSlot" : talkSlotMessage as NSDictionary]) } } } }

Schedule notification

#Devoxx #smartvoxx @sarbogast @eloudsa

import WatchConnectivity

@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate, WCSessionDelegate { var session: WCSession?

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool { if WCSession.isSupported() { session = WCSession.defaultSession() session?.delegate = self session?.activateSession() } return true }

func session(session: WCSession, didReceiveMessage message: [String : AnyObject], replyHandler: ([String : AnyObject]) -> Void) { self.updateLocalNotificationWithMessage(message) } func session(session: WCSession, didReceiveUserInfo userInfo: [String : AnyObject]) { self.updateLocalNotificationWithMessage(userInfo) } }

Back in iPhone's AppDelegate…

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 8: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 9: Custom notifications

#Devoxx #smartvoxx @sarbogast @eloudsa

Phone: Prepare and SendPhone

AlarmManagerAlarmReceiver

Custom notification

Action EventService

3

4

AlarmService

talkId, title, eventId, schedule, …1

.broadcast()

2

#Devoxx #smartvoxx @sarbogast @eloudsa

//CreateanintentforthereplyactionIntentactionIntent=newIntent(context,EventService.class);actionIntent.putExtras(bundle);PendingIntentactionPendingIntent=PendingIntent.getService(context,0,actionIntent, PendingIntent.FLAG_UPDATE_CURRENT); //CreatetheactionNotificationCompat.Actionaction=newNotificationCompat.Action.Builder(R.drawable.ic_calendar, context.getText(R.string.remove_event),actionPendingIntent).build();

Notification: Action to Service

#Devoxx #smartvoxx @sarbogast @eloudsa

//Addanotificationwiththesameactiononmobileandwatch NotificationCompat.BuildermBuilder=newNotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_logo) .setContentTitle(talk.getTitle()) .setContentText(information).setAutoCancel(true).setVibrate(newlong[]{1000,1000,1000,1000,1000,1000}) .setDefaults(Notification.DEFAULT_ALL) .addAction(action);

NotificationCompat.WearableExtenderwearableExtender=newNotificationCompat.WearableExtender(mBuilder.build()); Bitmapbitmap=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_black); wearableExtender.setBackground(bitmap);wearableExtender.extend(mBuilder);NotificationManagerCompatmanager=NotificationManagerCompat.from(context); manager.notify(notificationId,mBuilder.build());

Notification: Builder

#Devoxx #smartvoxx @sarbogast @eloudsa

Receiving notification

#Devoxx #smartvoxx @sarbogast @eloudsa

//Addanotificationwithanactiononlyvisibleonthewatch NotificationCompat.BuildermBuilder=newNotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_logo) .setContentTitle(talk.getTitle()) .setContentText(information).setAutoCancel(true).setVibrate(newlong[]{1000,1000,1000,1000,1000,1000}) .setDefaults(Notification.DEFAULT_ALL) .extend(newNotificationCompat.WearableExtender().addAction(action));

NotificationCompat.WearableExtenderwearableExtender=newNotificationCompat.WearableExtender(mBuilder.build()); Bitmapbitmap=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_black); wearableExtender.setBackground(bitmap);wearableExtender.extend(mBuilder);NotificationManagerCompatmanager=NotificationManagerCompat.from(context); manager.notify(notificationId,mBuilder.build());

Wearable-only action

#Devoxx #smartvoxx @sarbogast @eloudsa

Wearable-only

Wearable-only action

#Devoxx #smartvoxx @sarbogast @eloudsa

Watch: Action Remove

Phone-> EventService

#Devoxx #smartvoxx @sarbogast @eloudsa

Phone: EventServicePhone

Calendar

EventServiceremove Event

2

1

3

Event removed

#Devoxx #smartvoxx @sarbogast @eloudsa

<application>…

<serviceandroid:name=".service.EventService"/>

</application

Alarm: EventServiceUpdate the manifest of the phone

#Devoxx #smartvoxx @sarbogast @eloudsa

publicclassEventServiceextendsService{

@OverridepublicintonStartCommand(Intentintent,intflags,intstartId){…//removetheeventfromthecalendarCalendarHelpercalendarHelper=newCalendarHelper(this); calendarHelper.removeEvent(eventId);

…sendFavorite(talkId,0L);

..}

Phone: EventService

Demo

#Devoxx #smartvoxx @sarbogast @eloudsa

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 9: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 9: Notifications

• Schedule notifications on the iPhone

• Custom notification controller

#Devoxx #smartvoxx @sarbogast @eloudsa

private func updateLocalNotificationWithMessage(message:[String:AnyObject]){ if let talkSlot = message["talkSlot"] as? NSDictionary { let id = talkSlot["talkId"] as? String for notification in UIApplication.sharedApplication().scheduledLocalNotifications! { if let userInfo = notification.userInfo { if let talkId = userInfo["id"] as? String { if talkId == id { UIApplication.sharedApplication().cancelLocalNotification(notification) } } } } let favorite = talkSlot["favorite"] as? NSNumber if let fav = favorite?.boolValue where fav { let title = talkSlot["title"] as? String let room = talkSlot["room"] as? String let fromTimeMillis = talkSlot["fromTimeMillis"] as? NSNumber let fromTime = talkSlot["fromTime"] as? String let toTime = talkSlot["toTime"] as? String let date = NSDate(timeIntervalSince1970: fromTimeMillis!.doubleValue / 1000) let notification = UILocalNotification() notification.fireDate = date.dateByAddingTimeInterval(-10*60) notification.timeZone = NSTimeZone.localTimeZone() notification.userInfo = talkSlot as [NSObject : AnyObject] notification.alertTitle = title notification.alertBody = String(format: NSLocalizedString("From %@ to %@ in %@", comment: ""), arguments: [fromTime!, toTime!, room!]) UIApplication.sharedApplication().scheduleLocalNotification(notification) } } }

Actually schedule notifications

#Devoxx #smartvoxx @sarbogast @eloudsa

Custom notification controller

#Devoxx #smartvoxx @sarbogast @eloudsa

class NotificationController: WKUserNotificationInterfaceController { @IBOutlet var titleLabel: WKInterfaceLabel! @IBOutlet var trackLabel: WKInterfaceLabel! @IBOutlet var roomLabel: WKInterfaceLabel! @IBOutlet var timesLabel: WKInterfaceLabel! […] override func didReceiveLocalNotification(localNotification: UILocalNotification, withCompletion completionHandler: ((WKUserNotificationInterfaceType) -> Void)) { if let userInfo = localNotification.userInfo { self.titleLabel.setText(userInfo["title"] as? String) self.trackLabel.setText(userInfo["track"] as? String) self.roomLabel.setText(userInfo["room"] as? String) let fromTime = userInfo["fromTime"] as? String let toTime = userInfo["toTime"] as? String self.timesLabel.setText("\(fromTime!) - \(toTime!)") } completionHandler(.Custom) }

Custom notification controller

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 9: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 10:Glances and Complications

• Glance controller

• Complication controller

#Devoxx #smartvoxx @sarbogast @eloudsa

Glance controller

#Devoxx #smartvoxx @sarbogast @eloudsa

class GlanceController: WKInterfaceController { @IBOutlet var headerLabel: WKInterfaceLabel! @IBOutlet var subtitleLabel: WKInterfaceLabel! @IBOutlet var titleLabel: WKInterfaceLabel! @IBOutlet var roomLabel: WKInterfaceLabel! @IBOutlet var dateLabel: WKInterfaceLabel! var nextFavoriteSlots:[TalkSlot]? override func awakeWithContext(context: AnyObject?) { super.awakeWithContext(context) self.headerLabel.setText(NSLocalizedString("Next", comment: "")) self.subtitleLabel.setText(NSLocalizedString("in Devoxx 2015", comment: "")) }

}

Glance controller code

#Devoxx #smartvoxx @sarbogast @eloudsa

override func willActivate() { super.willActivate() self.nextFavoriteSlots = DataController.sharedInstance.getFavoriteTalksAfterDate(NSDate()) if let nextFavoriteSlots = self.nextFavoriteSlots where nextFavoriteSlots.count > 0 { let now = NSDate() var nextFavoriteSlot:TalkSlot? for talkSlot in nextFavoriteSlots { if talkSlot.fromTimeMillis?.doubleValue > now.timeIntervalSince1970 * 1000 { nextFavoriteSlot = talkSlot break } } if let nextFavoriteSlot = nextFavoriteSlot { self.titleLabel.setText(nextFavoriteSlot.title) self.roomLabel.setHidden(false) self.dateLabel.setHidden(false) self.roomLabel.setText(nextFavoriteSlot.roomName) let startDate = NSDate(timeIntervalSince1970: nextFavoriteSlot.fromTimeMillis!.doubleValue / 1000) let formatter = NSDateFormatter() formatter.dateStyle = NSDateFormatterStyle.LongStyle formatter.timeStyle = NSDateFormatterStyle.NoStyle let day = formatter.stringFromDate(startDate) self.dateLabel.setText("\(day), \(nextFavoriteSlot.fromTime!) - \(nextFavoriteSlot.toTime!)") } else { self.titleLabel.setText(NSLocalizedString("No more upcoming favorite talk.", comment: "")) self.roomLabel.setHidden(true) self.dateLabel.setHidden(true) } } else { self.titleLabel.setText(NSLocalizedString("No more upcoming favorite talk.", comment: "")) self.roomLabel.setHidden(true) self.dateLabel.setHidden(true) } }

Update glance data

#Devoxx #smartvoxx @sarbogast @eloudsa

import ClockKit

class ComplicationController: NSObject, CLKComplicationDataSource { func getSupportedTimeTravelDirectionsForComplication(complication: CLKComplication, withHandler handler: (CLKComplicationTimeTravelDirections) -> Void) { handler([.None]) }

func getCurrentTimelineEntryForComplication(complication: CLKComplication, withHandler handler: ((CLKComplicationTimelineEntry?) -> Void)) { handler(self.timelineEntryForNextFavoriteTalk()) }

private func timelineEntryForNextFavoriteTalk() -> CLKComplicationTimelineEntry? { let template = CLKComplicationTemplateModularLargeStandardBody() let now = NSDate() if let firstTalk = DataController.sharedInstance.getFirstTalk() { if now.timeIntervalSince1970 * 1000 < firstTalk.fromTimeMillis!.doubleValue { template.headerTextProvider = CLKRelativeDateTextProvider(date: NSDate(timeIntervalSince1970: firstTalk.fromTimeMillis!.doubleValue / 1000), style: .Natural, units: [.Day, .Hour]) template.body1TextProvider = CLKSimpleTextProvider(text: NSLocalizedString("until Devoxx 2015", comment:"")) } } return CLKComplicationTimelineEntry(date: now.dateByAddingTimeInterval(-60), complicationTemplate: template) } }

Complication controller

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 10: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 11: Release the App

#Devoxx #smartvoxx @sarbogast @eloudsa

Prepare the build

• Include all permissions of wearable into Phone

• Same package name and version number

#Devoxx #smartvoxx @sarbogast @eloudsa

Generate signed APK

Mobile embeds Wear

#Devoxx #smartvoxx @sarbogast @eloudsa

Mobile APK embeds WearablePhone App Module

Code

Resources

Wearable App

Watch App Module

Code

Resources

#Devoxx #smartvoxx @sarbogast @eloudsa

Publishing: Select Android Wear

#Devoxx #smartvoxx @sarbogast @eloudsa

Distribution

Companion App

Wearable App

Bluetooth

Companion App

Play Services

Android Wear

Wearable App

Smartvoxx

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 11: Done!

#Devoxx #smartvoxx @sarbogast @eloudsa

Step 11: Release the App

• Package the Apple Watch app with the iPhone app

• Release the iPhone app like any other

• Wait for review…

• Wait again…

• Wait some more…

#Devoxx #smartvoxx @sarbogast @eloudsa

A word about Pebble

• Language: either C or Javascript

• Development environment: either text editor or CloudPebble

• Platform support: both iOS and Android (+SDKs)

• Devices: Pebble Classic, Pebble Time, Pebble Time Round

• Distribution: via the Pebble app

#Devoxx #smartvoxx @sarbogast @eloudsa

staticboolload_shutter_group_list(){if(accessToken&&sizeof(accessToken)>0){DictionaryIterator*iter;app_message_outbox_begin(&iter);if(iter==NULL){APP_LOG(APP_LOG_LEVEL_DEBUG,"nulliter");returnfalse;}

Tupletmessage_type_tuple=TupletInteger(MESSAGE_TYPE,LOAD_SHUTTER_GROUP_LIST);dict_write_tuplet(iter,&message_type_tuple);Tupletaccess_token_tuple=TupletCString(ACCESS_TOKEN,accessToken);dict_write_tuplet(iter,&access_token_tuple);Tupletrefresh_token_tuple=TupletCString(REFRESH_TOKEN,refreshToken);dict_write_tuplet(iter,&refresh_token_tuple);Tupletsite_id_tuple=TupletInteger(SITE_ID,selected_site_id);dict_write_tuplet(iter,&site_id_tuple);dict_write_end(iter);

app_message_outbox_send();

returntrue;}else{returnfalse;}}

Pebble code

#Devoxx #smartvoxx @sarbogast @eloudsa

functionloadShutterGroupList(accessToken,refreshToken,args){ console.log("Loadingshuttergrouplistforaccesstoken"+accessToken+"andsite"+args[0]); varresponse; varreq=newXMLHttpRequest(); //buildtheGETrequest varurl="https://api.myfox.me:443/v2/site/"+args[0]+"/group/shutter/items?access_token="+accessToken; console.log("GETting"+url); req.open('GET',url,true); req.onload=function(e){ if(req.readyState==4){ //200-HTTPOK if(req.status==200){ console.log(req.responseText); response=JSON.parse(req.responseText); varshutterGroupList; if(response.status==='OK'){ shutterGroupList=response.payload.items;

varmsg={}; msg.messageType=MessageType.SHUTTER_GROUP_LIST; for(vari=0;i<shutterGroupList.length;i++){ varshutterGroup=shutterGroupList[i]; msg[''+shutterGroup.groupId]=shutterGroup.label; } console.log("SendingresponsebacktoPebble:"+JSON.stringify(msg)); Pebble.sendAppMessage(msg); }else{ console.log("StatusnotOK"); Pebble.sendAppMessage({messageType:MessageType.ERROR,errorMessage:"Couldnotloadshuttergroups."}); } }elseif(req.status==401&&refreshToken){ getNewAccessToken(refreshToken,loadShutterGroupList,args); }else{ console.log("Requestreturnederrorcode"+req.status.toString()); Pebble.sendAppMessage({messageType:MessageType.ERROR,errorMessage:"Couldnotloadshuttergroups."}); } } }; req.send(null);}

Pebble code

#Devoxx #smartvoxx @sarbogast @eloudsa

Summary

• Huge inequalities in terms of development platform ease-of-use

• Apple obviously took time to add abstraction layers that make development more expressive

• Short learning curve on Android Wear compared to Apple Watch

• Tooling support not up-to-date on Android

• Documentation is not really finished for both platforms

• Not all apps make sense on smartwatches

#Devoxx #smartvoxx @sarbogast @eloudsa

Apps that work on smartwatches• countdowns and timers

• status checks: what’s the temperature? what’s my next session? what’s the score of the game?

• remote controls: switch off the light, change the music, open my hotel room, pay for my shopping

• notification responders: invitation to a meeting -> what’s the meeting about, somebody sent me a message -> what does it say?

• data trackers: where am I? how many calories am I burning? what’s my speed?)

#Devoxx #smartvoxx @sarbogast @eloudsa

Apps that don’t make sense

•  games of any kind

•  any long reading (news, books, etc.)

•  ecommerce

•  video or image viewing

•  anything that requires text input

#Devoxx #smartvoxx @sarbogast @eloudsa

One more thing …

#Devoxx #smartvoxx @sarbogast @eloudsa

Smartvoxx on GithubAvailable in Black … … and White

Coming soon…