SlideShare une entreprise Scribd logo
1  sur  285
Télécharger pour lire hors ligne
@sarbogast @eloudsa#Devoxx #smartvoxx
Apps On Your Wrist
Sé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
Disclaimer
We 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
Requirements
g.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

},

…

]

}
Schedules
http://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"
}
},
…
}
]}
Slot
http://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,	9th	November	2015	
…	tuesday	-	Tuesday,	10th	November	2015	
…	wednesday	-	Wednesday,	11th	November	2015	
…	thursday	-	Thursday,	12th	November	2015	
…	friday	-	Friday,	13th	November	2015
#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 Services
Data Layer
Message API
Data API
Node API
#Devoxx #smartvoxx @sarbogast @eloudsa
Google API Client
Device
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 Layer
Bluetooth/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
<Button

				android:id="@+id/getSchedules"

				style="?android:attr/buttonStyleSmall"

				android:layout_width="wrap_content"

				android:layout_height="wrap_content"

				android:layout_gravity="center_horizontal"

				android:text="Get	Schedules"	/>	
Layout: Add a button
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	ScheduleActivity	extends	Activity	{	
private	GoogleApiClient	mApiClient;	
…	
@Override

protected	void	onStart()	{

				super.onStart();



				mApiClient	=	new	GoogleApiClient.Builder(this)

												.addApi(Wearable.API)

												.build();



				mApiClient.connect();

}





@Override

protected	void	onStop()	{

				if	((mApiClient	!=	null)	&&	(mApiClient.isConnected()))	{

								mApiClient.disconnect();

				}



				super.onStop();

}
Activity: Google API Client
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	ScheduleActivity	extends	Activity	{	
private	final	String	SCHEDULES_PATH	=	"/schedules";	
…	
@Override

protected	void	onCreate(Bundle	savedInstanceState)	{	
…	
			stub.setOnLayoutInflatedListener(new	WatchViewStub.OnLayoutInflatedListener()	{

				@Override

				public	void	onLayoutInflated(WatchViewStub	stub)	{

								mTextView	=	(TextView)	stub.findViewById(R.id.text);



								stub.findViewById(R.id.getSchedules).setOnClickListener(new	View.OnClickListener()	{

												@Override

												public	void	onClick(View	v)	{

																sendMessage(SCHEDULES_PATH,	"dummy");

												}

								});

				}

});	
}

Activity: Button Listener
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	ScheduleActivity	extends	Activity	{	
…	
private	void	sendMessage(final	String	path,	final	String	message)	{

				new	Thread(new	Runnable()	{

								@Override

								public	void	run()	{

												//	broadcast	the	message	to	all	connected	devices

												final	NodeApi.GetConnectedNodesResult	nodes	=	Wearable.NodeApi.getConnectedNodes(mApiClient).await();

												for	(Node	node	:	nodes.getNodes())	{

																Wearable.MessageApi.sendMessage(mApiClient,	node.getId(),	path,	message.getBytes()).await();



												}

								}

				}).start();

}	
Activity: Send Message
#Devoxx #smartvoxx @sarbogast @eloudsa
…	
dependencies	{	
compile	fileTree(dir:	'libs',	include:	['*.jar'])

wearApp	project(':wear')

compile	'com.android.support:appcompat-v7:22.2.1'	
compile	‘com.google.android.gms:play-services:8.1.0’	
					
//	Rest	API	Client

compile	‘com.squareup.retrofit:retrofit:1.9.0’	
}	
REST API Client library: Retrofit
build.gradle
#Devoxx #smartvoxx @sarbogast @eloudsa
package	net.noratek.smartvoxxwear.rest.model;



import	java.util.List;





public	class	Schedules	{



			private	List<Link>	links;



			public	List<Link>	getLinks()	{

								return	links;

				}

}

Model: Schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
package	net.noratek.smartvoxxwear.rest.model;



/**

	*	Created	by	eloudsa	on	06/09/15.

	*/

public	class	Link	{



				private	String	href;

				private	String	rel;

				private	String	title;





				public	Link(String	href,	String	rel,	String	title)	{

								this.href	=	href;

								this.rel	=	rel;

								this.title	=	title;

				}



				//	Getters	and	Setters

			…	
}

Model: Link
#Devoxx #smartvoxx @sarbogast @eloudsa
package	net.noratek.smartvoxxwear.rest.service;



import	net.noratek.smartvoxxwear.rest.model.Schedules;



import	retrofit.Callback;

import	retrofit.http.GET;

import	retrofit.http.Path;



/**

	*	Created	by	eloudsa	on	30/10/15.

	*/

public	interface	DevoxxApi	{





				@GET("/conferences/{conference}/schedules")

				void	getSchedules(@Path("conference")	String	conference,	Callback<Schedules>	callback);

}

REST Endpoint for Schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
WearableListenerService
• New class WearService extended from
WearableListenerService
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<manifest	xmlns:android="http://schemas.android.com/apk/res/android"

				package="net.noratek.smartvoxxwear"	>	
<uses-permission	android:name="android.permission.INTERNET"/>	


			<application

								…

								<!--	Android	Wear	Service	-->

								<service	android:name=".service.WearService">

												<intent-filter>

																<action	android:name="com.google.android.gms.wearable.BIND_LISTENER"	/>

												</intent-filter>

								</service>





				</application>



</manifest>

Service: Adapt the manifest
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	WearService	extends	WearableListenerService	{	
…	
@Override

public	void	onMessageReceived(MessageEvent	messageEvent)	{



				//	Processing	the	incoming	message

				String	path	=	messageEvent.getPath();

				String	data	=	new	String(messageEvent.getData());



				if	(path.equalsIgnoreCase(SCHEDULES_PATH))	{

								retrieveSchedules();

								return;

				}

}

Process “/schedules”
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	WearService	extends	WearableListenerService	{

…

	//	Retrieve	schedules	from	Devoxx

	private	void	retrieveSchedules()	{

				//	retrieve	the	schedules	list	from	the	server

				Callback	callback	=	new	Callback()	{

								@Override

								public	void	success(Object	o,	Response	response)	{

												//	retrieve	schedule	from	REST

												Schedules	scheduleList	=	(Schedules)	o;

												if	(scheduleList	==	null)	{

																Log.d(TAG,	"No	schedules!");

																return;

												}



												List<Link>	links	=	scheduleList.getLinks();



												for	(Link	link	:	links)	{

																Log.d(TAG,	Utils.getLastPartUrl(link.getHref())	+	"	-	"	+	link.getTitle());

												}

								}



								@Override

								public	void	failure(RetrofitError	retrofitError)	{

												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,	9th	November	2015	
…	tuesday	-	Tuesday,	10th	November	2015	
…	wednesday	-	Wednesday,	11th	November	2015	
…	thursday	-	Thursday,	12th	November	2015	
…	friday	-	Friday,	13th	November	2015
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
@Override

protected	void	onResume()	{

				super.onResume();



				//	Retrieve	the	list	of	schedules

				sendMessage(SCHEDULES_PATH,	"get	list	of	schedules");	
}

Display schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	WearService	extends	WearableListenerService	{	
//	send	Schedules	to	the	watch

private	void	sendSchedules(List<Link>	schedules)	{

				final	PutDataMapRequest	putDataMapRequest	=	PutDataMapRequest.create(SCHEDULES_PATH);



				ArrayList<DataMap>	schedulesDataMap	=	new	ArrayList<>();



				//	process	each	schedule

				for	(Link	schedule	:	schedules)	{



								final	DataMap	scheduleDataMap	=	new	DataMap();



								//	process	and	push	schedule's	data	
									
								//	We	need	to	add	a	timestamp	to	force	a	onDataChanged	event	on	the	remote	device.

								scheduleDataMap.putString("timestamp",	new	Date().toString());	
								scheduleDataMap.putString("day",	Utils.getLastPartUrl(schedule.getHref()));

								scheduleDataMap.putString("title",	schedule.getTitle());



								schedulesDataMap.add(scheduleDataMap);

				}



				//	store	the	list	in	the	datamap	to	send	it	to	the	watch

				putDataMapRequest.getDataMap().putDataMapArrayList("/list",	schedulesDataMap);



				//	send	the	list

				if	(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
@Override

protected	void	onCreate(Bundle	savedInstanceState)	{

				…

												//	Listview	component

												listView	=	(WearableListView)	findViewById(R.id.wearable_list);



												//	Assign	the	adapter

												listViewAdapter	=	new	ListViewAdapter(ScheduleActivity.this,	new	
ArrayList<Schedule>());

												listView.setAdapter(listViewAdapter);

…	
}

ListViewAdapter
#Devoxx #smartvoxx @sarbogast @eloudsa
WearableListView
#Devoxx #smartvoxx @sarbogast @eloudsa
//	Inner	class	providing	the	WearableListview's	adapter

public	class	ListViewAdapter	extends	WearableListView.Adapter	{

…	
//	Create	new	views	for	list	items

//	(invoked	by	the	WearableListView's	layout	manager)

@Override

public	WearableListView.ViewHolder	onCreateViewHolder(ViewGroup	parent,

																																																						int	viewType)	{

				//	Inflate	our	custom	layout	for	list	items

				return	new	ItemViewHolder(new	SettingsItemView(mContext));

}	
…
Animation
#Devoxx #smartvoxx @sarbogast @eloudsa
public	final	class	SettingsItemView	extends	FrameLayout	implements	
WearableListView.OnCenterProximityListener	{



				private	TextView	description;



				public	SettingsItemView(Context	context)	{

								super(context);

								View.inflate(context,	R.layout.schedule_row_activity,	this);



								description	=	(TextView)	findViewById(R.id.description);

				}



				@Override

				public	void	onCenterPosition(boolean	b)	{

								description.animate().scaleX(1f).scaleY(1f).alpha(1);

				}



				@Override

				public	void	onNonCenterPosition(boolean	b)	{

								description.animate().scaleX(0.8f).scaleY(0.8f).alpha(0.6f);

				}

}	
Animation
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<LinearLayout	xmlns:android="http://schemas.android.com/apk/res/android"

				…>



				<TextView

								android:id="@+id/title"

								…/>





				<RelativeLayout

								android:layout_width="fill_parent"

								android:layout_height="fill_parent">



								<android.support.wearable.view.WearableListView

												android:id="@+id/wearable_list"

												…>

								</android.support.wearable.view.WearableListView>



								<ProgressBar

												android:id="@+id/progressBar"

											…	/>



				</RelativeLayout>

</LinearLayout>	
Layout: schedule_rect_activity
1
2
3
4
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<merge	xmlns:android="http://schemas.android.com/apk/res/android">





				<TextView

								android: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
@Override

public	void	onBindViewHolder(WearableListView.ViewHolder	holder,

																													int	position)	{



				//	retrieve	the	text	view

				ItemViewHolder	itemHolder	=	(ItemViewHolder)	holder;

				TextView	view	=	itemHolder.textView;



				//	retrieve,	transform	and	display	the	schedule's	day

				Schedule	schedule	=	mDataset.get(position);

				String	scheduleDay	=	schedule.getTitle().replace(",",	"n");

				view.setText(scheduleDay);



				//	replace	list	item's	metadata

				holder.itemView.setTag(schedule);

}

Store Schedule’s data
1
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
1
2
Fetch from local cache
MessageApi
sendMessage()
3
4
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	WearService	extends	WearableListenerService	{	
//	send	Schedules	to	the	watch

private	void	sendSchedules(List<Link>	schedules)	{

				final	PutDataMapRequest	putDataMapRequest	=	PutDataMapRequest.create(SCHEDULES_PATH);



				ArrayList<DataMap>	schedulesDataMap	=	new	ArrayList<>();



				//	process	each	schedule

				for	(Link	schedule	:	schedules)	{



								final	DataMap	scheduleDataMap	=	new	DataMap();



								//	process	and	push	schedule's	data

								scheduleDataMap.putString("day",	Utils.getLastPartUrl(schedule.getHref()));

								scheduleDataMap.putString("title",	schedule.getTitle());



								schedulesDataMap.add(scheduleDataMap);

				}



				//	store	the	list	in	the	datamap	to	send	it	to	the	watch

				putDataMapRequest.getDataMap().putDataMapArrayList("/list",	schedulesDataMap);



				//	send	the	list

				if	(mApiClient.isConnected())	{

								Wearable.DataApi.putDataItem(mApiClient,	putDataMapRequest.asPutDataRequest());

				}

}
Changes on Phone: No timestamp
1
#Devoxx #smartvoxx @sarbogast @eloudsa
@Override

protected	void	onResume()	{

				super.onResume();



				//	Retrieve	and	display	the	list	of	schedules

				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
<FrameLayout	xmlns: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.GridViewPager	xmlns: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>

Layout res/layout/talk_activity.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
Talk detail: Custom layout
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="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>	
Layout res/layout/talk_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<RelativeLayout

				xmlns:android="http://schemas.android.com/apk/res/android"

				android:layout_width="fill_parent"

				android:layout_height="fill_parent"

				android:paddingTop="15dp">



				<TextView

								android:id="@+id/title"

								android:layout_width="fill_parent"

								
….	
Layout res/layout/talk_rect_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<android.support.wearable.view.BoxInsetLayout		
				xmlns:android="http://schemas.android.com/apk/res/android"

				android:layout_width="match_parent"

				android:layout_height="match_parent"

				android:padding="15dp">



								<RelativeLayout

												android:layout_width="match_parent"

												android:layout_height="match_parent">



												<TextView

																android:id="@+id/title"

Layout res/layout/talk_round_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
Summary: Card
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<android.support.wearable.view.CardScrollView		
xmlns: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.CardFrame

								android:id="@+id/card_frame"

								…>



								<LinearLayout

												android:layout_width="wrap_content"

												android:layout_height="wrap_content"

												android:orientation="vertical">



									<TextView

																android:id="@+id/title"

Summary: Layout
#Devoxx #smartvoxx @sarbogast @eloudsa
Summary: Expand content
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	TalkSummaryFragment	extends	Fragment	{	
…	
description.setOnClickListener(new	View.OnClickListener()	{

				@Override

				public	void	onClick(View	v)	{

								if	(mTalkSummary	==	null)	{

												return;

								}



								if	(mEllipsize)	{

												description.setText(mTalkSummary);

								}	else	{

												description.setText(StringUtils.abbreviate(mTalkSummary,	ELLIPSIS_SIZE));

								}

								mEllipsize	=	!mEllipsize;	


								//	Force	the	CardScrollView	to	reset	to	its	initial	position

								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
Events
Fragments
Events
#Devoxx #smartvoxx @sarbogast @eloudsa
EventBus (BusWear)
#Devoxx #smartvoxx @sarbogast @eloudsa
EventBus (BusWear)
#Devoxx #smartvoxx @sarbogast @eloudsa
dependencies	{

				compile	fileTree(dir:	'libs',	include:	['*.jar'])

				compile	'com.google.android.support:wearable:1.3.0'

				compile	'com.google.android.gms:play-services-wearable:8.1.0'



				//	Event	Bus:	BusWear

				compile	'pl.tajchert:buswear:0.9.5'



			…

}	
EventBus
build.grade
#Devoxx #smartvoxx @sarbogast @eloudsa
EventBus.getDefault().postLocal(new	TalkEvent(mTalk));	
EventBus - Sending events
#Devoxx #smartvoxx @sarbogast @eloudsa
public	void	onEvent(AddFavoriteEvent	addFavoritesEvent)	{

…	
}
EventBus - Receive events
Required to register/unregister to the bus
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	TalkActivity	extends	Activity	…	{	
@Override

protected	void	onStart()	{

				…

				EventBus.getDefault().register(this);

}



@Override

protected	void	onStop()	{

				EventBus.getDefault().unregister(this);

				…	
}
EventBus - (Un)Register
#Devoxx #smartvoxx @sarbogast @eloudsa
Twitter: Confirmation animation
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<manifest	xmlns:android="http://schemas.android.com/apk/res/android"

				package="net.noratek.smartvoxxwear">	
…	
<activity	android:name="android.support.wearable.activity.ConfirmationActivity"	/>	
…
Confirmation animation
Update the manifest of the watch
#Devoxx #smartvoxx @sarbogast @eloudsa
@Override

public	View	onCreateView(LayoutInflater	inflater,	ViewGroup	container,

																									Bundle	savedInstanceState)	{	
			mainView.findViewById(R.id.twitterIcon).setOnClickListener(new	View.OnClickListener()	
{

							@Override

							public	void	onClick(View	v)	{

											startConfirmationActivity(ConfirmationActivity.OPEN_ON_PHONE_ANIMATION,	
getString(R.string.confirmation_open_on_phone));	


											EventBus.getDefault().postLocal(new	ConfirmationEvent(TWITTER_PATH,	(String)	
mainView.findViewById(R.id.twitterIcon).getTag()));

							}

			});
Confirmation animation (Listener)
1
2
#Devoxx #smartvoxx @sarbogast @eloudsa
private	void	startConfirmationActivity(int	animationType,	String	message)	{	


				Intent	confirmationActivity	=	new	Intent(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
public	class	WearService	extends	WearableListenerService	{	
…	
private	void	followOnTwitter(String	inputData)	{

				String	twitterName	=	inputData	==	null	?	""	:	inputData.trim().toLowerCase();

				twitterName	=	twitterName.replaceFirst("@",	"");

			if	(twitterName.isEmpty())	{

								return;

				}



				Intent	intent	=	new	Intent(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 GridViewPager
CardCustom
Custom Custom
WearableListView
Step 7: Done!
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 7: Round
CardCustom
Custom Custom
WearableListView
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: Favorites
Phone Watch
1
2
3
4
eventId
remove
read
add
Calendar
remove
read
add
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Reminders
Phone
1
Calendar
add
AlarmManager
2
10 minutes
before
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Alarm Service
Phone
1
Calendar
add
AlarmManager
2
10 minutes
before
AlarmService
talkId, title,
eventId,
schedule, …
3
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Wake-up
Phone
AlarmManager
AlarmService
talkId, title,
eventId,
schedule, …1
AlarmReceiver
talkId, title,
eventId,
schedule, …
.broadcast()
2
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xml	version="1.0"	encoding="utf-8"?>

<manifest	xmlns:android="http://schemas.android.com/apk/res/android"

				package="net.noratek.smartvoxxwear"	>



			<!--	Add,	Read	and	remove	favorites	on	the	calendar	-->

				<uses-permission	android:name="android.permission.READ_CALENDAR"	/>

				<uses-permission	android:name="android.permission.WRITE_CALENDAR"	/>

Calendar: Add permissions
Update the manifest of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
<application>	
…	
<service	android:name=".alarm.AlarmService"/>



<receiver	android:name=".alarm.AlarmReceiver">

				<intent-filter>

								<action	
android:name="net.noratek.smartvoxxwear.AlarmService.BROADCAST"	/>

				</intent-filter>

</receiver>	
</application
Alarm: Service, Receiver
Update 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 Send
Phone
AlarmManager
AlarmReceiver
Custom notification
Action EventService
3
4
AlarmService
talkId, title,
eventId,
schedule, …1
.broadcast()
2
#Devoxx #smartvoxx @sarbogast @eloudsa
//	Create	an	intent	for	the	reply	action

Intent	actionIntent	=	new	Intent(context,	EventService.class);

actionIntent.putExtras(bundle);

PendingIntent	actionPendingIntent	=

								PendingIntent.getService(context,	0,	actionIntent,

																PendingIntent.FLAG_UPDATE_CURRENT);



//	Create	the	action

NotificationCompat.Action	action	=

								new	NotificationCompat.Action.Builder(R.drawable.ic_calendar,

																context.getText(R.string.remove_event),	actionPendingIntent)

																.build();	
Notification: Action to Service
#Devoxx #smartvoxx @sarbogast @eloudsa
//	Add	a	notification	with	the	same	action	on	mobile	and	watch

NotificationCompat.Builder	mBuilder	=	new	NotificationCompat.Builder(context)

								.setSmallIcon(R.drawable.ic_logo)

								.setContentTitle(talk.getTitle())

								.setContentText(information)

								.setAutoCancel(true)

								.setVibrate(new	long[]{1000,	1000,	1000,	1000,	1000,	1000})

								.setDefaults(Notification.DEFAULT_ALL)

								.addAction(action);

NotificationCompat.WearableExtender	wearableExtender	=	new	NotificationCompat.WearableExtender(mBuilder.build());



Bitmap	bitmap	=	BitmapFactory.decodeResource(context.getResources(),	R.drawable.ic_black);

wearableExtender.setBackground(bitmap);



wearableExtender.extend(mBuilder);



NotificationManagerCompat	manager	=	NotificationManagerCompat.from(context);

manager.notify(notificationId,	mBuilder.build());
Notification: Builder
#Devoxx #smartvoxx @sarbogast @eloudsa
Receiving notification
#Devoxx #smartvoxx @sarbogast @eloudsa
//	Add	a	notification	with	an	action	only	visible	on	the	watch

NotificationCompat.Builder	mBuilder	=	new	NotificationCompat.Builder(context)

									.setSmallIcon(R.drawable.ic_logo)

									.setContentTitle(talk.getTitle())

									.setContentText(information)

									.setAutoCancel(true)

									.setVibrate(new	long[]{1000,	1000,	1000,	1000,	1000,	1000})

									.setDefaults(Notification.DEFAULT_ALL)

									.extend(new	NotificationCompat.WearableExtender().addAction(action));	
NotificationCompat.WearableExtender	wearableExtender	=	new	NotificationCompat.WearableExtender(mBuilder.build());



Bitmap	bitmap	=	BitmapFactory.decodeResource(context.getResources(),	R.drawable.ic_black);

wearableExtender.setBackground(bitmap);



wearableExtender.extend(mBuilder);



NotificationManagerCompat	manager	=	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: EventService
Phone
Calendar
EventService
remove Event
2
1
3
Event removed
#Devoxx #smartvoxx @sarbogast @eloudsa
<application>	
…	
<service	android:name=".service.EventService"/>	
</application
Alarm: EventService
Update the manifest of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
public	class	EventService	extends	Service	{	
@Override

public	int	onStartCommand(Intent	intent,	int	flags,	int	startId)	{	
…	
//	remove	the	event	from	the	calendar

CalendarHelper	calendarHelper	=	new	CalendarHelper(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 Wearable
Phone 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
static	bool	load_shutter_group_list()	{	
				if(accessToken	&&	sizeof(accessToken)	>	0)	{	
								DictionaryIterator	*iter;	
								app_message_outbox_begin(&iter);	
								if	(iter	==	NULL)	{	
												APP_LOG(APP_LOG_LEVEL_DEBUG,	"null	iter");	
												return	false;	
								}	
								Tuplet	message_type_tuple	=	TupletInteger(MESSAGE_TYPE,	LOAD_SHUTTER_GROUP_LIST);	
								dict_write_tuplet(iter,	&message_type_tuple);	
								Tuplet	access_token_tuple	=	TupletCString(ACCESS_TOKEN,	accessToken);	
								dict_write_tuplet(iter,	&access_token_tuple);	
								Tuplet	refresh_token_tuple	=	TupletCString(REFRESH_TOKEN,	refreshToken);	
								dict_write_tuplet(iter,	&refresh_token_tuple);	
								Tuplet	site_id_tuple	=	TupletInteger(SITE_ID,	selected_site_id);	
								dict_write_tuplet(iter,	&site_id_tuple);	
								dict_write_end(iter);	
								app_message_outbox_send();	
								return	true;	
				}	else	{	
								return	false;	
				}	
}
Pebble code
#Devoxx #smartvoxx @sarbogast @eloudsa
function	loadShutterGroupList(accessToken,	refreshToken,	args)	{	
	 console.log("Loading	shutter	group	list	for	access	token	"	+	accessToken	+	"	and	site	"	+	args[0]);	
	 var	response;	
			 var	req	=	new	XMLHttpRequest();	
			 //	build	the	GET	request	
			 var	url	=	"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	-	HTTP	OK	
	 								 if(req.status	==	200)	{	
	 									 	 	 console.log(req.responseText);	
	 									 	 	 response	=	JSON.parse(req.responseText);	
	 									 	 	 var	shutterGroupList;	
	 									 	 	 if	(response.status	===	'OK')	{	 									 	 	 	
	 											 	 	 	 shutterGroupList	=	response.payload.items;	
	 	 	 	 		 var	msg	=	{};	
	 	 	 	 	 msg.messageType	=	MessageType.SHUTTER_GROUP_LIST;	
	 	 	 	 											 for(var	i	=	0;	i	<	shutterGroupList.length;	i++){	
	 	 	 	 											 	 var	shutterGroup	=	shutterGroupList[i];	
	 	 	 	 											 	 msg[''	+	shutterGroup.groupId]	=	shutterGroup.label;	
	 	 	 	 											 }	
	 	 	 	 											 console.log("Sending	response	back	to	Pebble:	"	+	JSON.stringify(msg));	
	 	 	 	 											 Pebble.sendAppMessage(msg);	
	 									 	 	 }	else	{	
	 									 	 	 	 console.log("Status	not	OK");	
	 									 	 	 	 Pebble.sendAppMessage({messageType:MessageType.ERROR,	errorMessage:"Could	not	load	shutter	groups."});	
	 									 	 	 }	
	 								 }	else	if(req.status	==	401	&&	refreshToken){	
	 								 	 getNewAccessToken(refreshToken,	loadShutterGroupList,	args);	
	 								 }	else	{	
	 									 	 	 console.log("Request	returned	error	code	"	+	req.status.toString());	
	 									 	 	 Pebble.sendAppMessage({messageType:MessageType.ERROR,	errorMessage:"Could	not	load	shutter	groups."});	
	 								 }	
					 	 }	
			 };	
			 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 Github
Available in Black … … and White
Coming soon…

Contenu connexe

En vedette

CYPNAVAL 2013-Press Release
CYPNAVAL 2013-Press ReleaseCYPNAVAL 2013-Press Release
CYPNAVAL 2013-Press ReleaseVasilis Zomenos
 
WC 2015 Leadership lessons
WC 2015 Leadership lessonsWC 2015 Leadership lessons
WC 2015 Leadership lessonsGirish Batra
 
How-physical-arbitrage-works
How-physical-arbitrage-worksHow-physical-arbitrage-works
How-physical-arbitrage-worksGE 94
 
Ядерна модель атома
Ядерна модель атомаЯдерна модель атома
Ядерна модель атомаYuriy Lynnyk
 
Présentation projet ehpad vouziers
Présentation projet ehpad vouziersPrésentation projet ehpad vouziers
Présentation projet ehpad vouziersflorineherbillon
 
Dimensions of grand strategies
Dimensions of grand strategiesDimensions of grand strategies
Dimensions of grand strategiesAnand Dabasara
 
Блокчейн: Разрыв Шаблона v.3
Блокчейн: Разрыв Шаблона v.3Блокчейн: Разрыв Шаблона v.3
Блокчейн: Разрыв Шаблона v.3Dima Starodubcev
 
Exercicios de pressing e posse de bola
Exercicios de pressing e posse de bolaExercicios de pressing e posse de bola
Exercicios de pressing e posse de bolaPedro Sousa
 

En vedette (12)

Prova presentazione gio e miriam
Prova presentazione gio e miriamProva presentazione gio e miriam
Prova presentazione gio e miriam
 
CYPNAVAL 2013-Press Release
CYPNAVAL 2013-Press ReleaseCYPNAVAL 2013-Press Release
CYPNAVAL 2013-Press Release
 
WC 2015 Leadership lessons
WC 2015 Leadership lessonsWC 2015 Leadership lessons
WC 2015 Leadership lessons
 
How-physical-arbitrage-works
How-physical-arbitrage-worksHow-physical-arbitrage-works
How-physical-arbitrage-works
 
Ядерна модель атома
Ядерна модель атомаЯдерна модель атома
Ядерна модель атома
 
Gracious city 3
Gracious city 3Gracious city 3
Gracious city 3
 
Results
ResultsResults
Results
 
Présentation projet ehpad vouziers
Présentation projet ehpad vouziersPrésentation projet ehpad vouziers
Présentation projet ehpad vouziers
 
Cyberterrorism
CyberterrorismCyberterrorism
Cyberterrorism
 
Dimensions of grand strategies
Dimensions of grand strategiesDimensions of grand strategies
Dimensions of grand strategies
 
Блокчейн: Разрыв Шаблона v.3
Блокчейн: Разрыв Шаблона v.3Блокчейн: Разрыв Шаблона v.3
Блокчейн: Разрыв Шаблона v.3
 
Exercicios de pressing e posse de bola
Exercicios de pressing e posse de bolaExercicios de pressing e posse de bola
Exercicios de pressing e posse de bola
 

Similaire à Apps on your Wrist

Serverless: when functions and GitOps collide
Serverless: when functions and GitOps collideServerless: when functions and GitOps collide
Serverless: when functions and GitOps collideEdward Wilde
 
Strategies for Mobile eLearning
Strategies for Mobile eLearningStrategies for Mobile eLearning
Strategies for Mobile eLearningeaselsolutions
 
Nebraska Trainer's Institute eLearning Presentaiotn
Nebraska Trainer's Institute eLearning PresentaiotnNebraska Trainer's Institute eLearning Presentaiotn
Nebraska Trainer's Institute eLearning Presentaiotneaselsolutions
 
Azure ML: from basic to integration with custom applications
Azure ML: from basic to integration with custom applicationsAzure ML: from basic to integration with custom applications
Azure ML: from basic to integration with custom applicationsDavide Mauri
 
Building Your App SDK with Swift
Building Your App SDK with SwiftBuilding Your App SDK with Swift
Building Your App SDK with SwiftJordan Yaker
 
Global Azure Bootcamp - Vancouver (2018) - Testing with Containers
Global Azure Bootcamp - Vancouver (2018) - Testing with ContainersGlobal Azure Bootcamp - Vancouver (2018) - Testing with Containers
Global Azure Bootcamp - Vancouver (2018) - Testing with ContainersDen Delimarsky
 
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...JAXLondon2014
 
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"Daniel Bryant
 
Eskills4change by Fondazione Mondo Digitale
Eskills4change by Fondazione Mondo DigitaleEskills4change by Fondazione Mondo Digitale
Eskills4change by Fondazione Mondo DigitaleAngelo Gino Varrati
 
Flare - tech-intro-for-paris-hackathon
Flare - tech-intro-for-paris-hackathonFlare - tech-intro-for-paris-hackathon
Flare - tech-intro-for-paris-hackathonCisco DevNet
 
Introduction to New Age Applications with Kendo UI
Introduction to New Age Applications with Kendo UIIntroduction to New Age Applications with Kendo UI
Introduction to New Age Applications with Kendo UIAbhishek Kant
 
Devops Recto-Verso @ DevoxxMA
Devops Recto-Verso @ DevoxxMADevops Recto-Verso @ DevoxxMA
Devops Recto-Verso @ DevoxxMAArnaud Héritier
 
Why Do Mobile Projects Fail?
Why Do Mobile Projects Fail?Why Do Mobile Projects Fail?
Why Do Mobile Projects Fail?Indiginox
 
Getting Started with Mobile Websites if You Don't Know Code
Getting Started with Mobile Websites if You Don't Know CodeGetting Started with Mobile Websites if You Don't Know Code
Getting Started with Mobile Websites if You Don't Know CodeCarli Spina
 
Philly CocoaHeads 20160414 - Building Your App SDK With Swift
Philly CocoaHeads 20160414 - Building Your App SDK With SwiftPhilly CocoaHeads 20160414 - Building Your App SDK With Swift
Philly CocoaHeads 20160414 - Building Your App SDK With SwiftJordan Yaker
 
Going crazy with docker multi stage build - Jorge Arteiro
Going crazy with docker multi stage build - Jorge ArteiroGoing crazy with docker multi stage build - Jorge Arteiro
Going crazy with docker multi stage build - Jorge ArteiroJorge Arteiro
 
Mobeers waterloo-2011
Mobeers waterloo-2011Mobeers waterloo-2011
Mobeers waterloo-2011Brian LeRoux
 
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...SOASTA
 
Angular mobile angular_u
Angular mobile angular_uAngular mobile angular_u
Angular mobile angular_uDoris Chen
 

Similaire à Apps on your Wrist (20)

Serverless: when functions and GitOps collide
Serverless: when functions and GitOps collideServerless: when functions and GitOps collide
Serverless: when functions and GitOps collide
 
Strategies for Mobile eLearning
Strategies for Mobile eLearningStrategies for Mobile eLearning
Strategies for Mobile eLearning
 
Nebraska Trainer's Institute eLearning Presentaiotn
Nebraska Trainer's Institute eLearning PresentaiotnNebraska Trainer's Institute eLearning Presentaiotn
Nebraska Trainer's Institute eLearning Presentaiotn
 
Azure ML: from basic to integration with custom applications
Azure ML: from basic to integration with custom applicationsAzure ML: from basic to integration with custom applications
Azure ML: from basic to integration with custom applications
 
Building Your App SDK with Swift
Building Your App SDK with SwiftBuilding Your App SDK with Swift
Building Your App SDK with Swift
 
Global Azure Bootcamp - Vancouver (2018) - Testing with Containers
Global Azure Bootcamp - Vancouver (2018) - Testing with ContainersGlobal Azure Bootcamp - Vancouver (2018) - Testing with Containers
Global Azure Bootcamp - Vancouver (2018) - Testing with Containers
 
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...
Moving to a DevOps mode - easy, hard or just plain terrifying? - Daniel Bryan...
 
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"
JAX London 2014 "Moving to DevOps Mode: easy, hard or just plain terrifying?"
 
Xamarin tools
Xamarin toolsXamarin tools
Xamarin tools
 
Eskills4change by Fondazione Mondo Digitale
Eskills4change by Fondazione Mondo DigitaleEskills4change by Fondazione Mondo Digitale
Eskills4change by Fondazione Mondo Digitale
 
Flare - tech-intro-for-paris-hackathon
Flare - tech-intro-for-paris-hackathonFlare - tech-intro-for-paris-hackathon
Flare - tech-intro-for-paris-hackathon
 
Introduction to New Age Applications with Kendo UI
Introduction to New Age Applications with Kendo UIIntroduction to New Age Applications with Kendo UI
Introduction to New Age Applications with Kendo UI
 
Devops Recto-Verso @ DevoxxMA
Devops Recto-Verso @ DevoxxMADevops Recto-Verso @ DevoxxMA
Devops Recto-Verso @ DevoxxMA
 
Why Do Mobile Projects Fail?
Why Do Mobile Projects Fail?Why Do Mobile Projects Fail?
Why Do Mobile Projects Fail?
 
Getting Started with Mobile Websites if You Don't Know Code
Getting Started with Mobile Websites if You Don't Know CodeGetting Started with Mobile Websites if You Don't Know Code
Getting Started with Mobile Websites if You Don't Know Code
 
Philly CocoaHeads 20160414 - Building Your App SDK With Swift
Philly CocoaHeads 20160414 - Building Your App SDK With SwiftPhilly CocoaHeads 20160414 - Building Your App SDK With Swift
Philly CocoaHeads 20160414 - Building Your App SDK With Swift
 
Going crazy with docker multi stage build - Jorge Arteiro
Going crazy with docker multi stage build - Jorge ArteiroGoing crazy with docker multi stage build - Jorge Arteiro
Going crazy with docker multi stage build - Jorge Arteiro
 
Mobeers waterloo-2011
Mobeers waterloo-2011Mobeers waterloo-2011
Mobeers waterloo-2011
 
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...
How to Set Up Mobile Continuous Integration with Real Devices: CloudBees & SO...
 
Angular mobile angular_u
Angular mobile angular_uAngular mobile angular_u
Angular mobile angular_u
 

Plus de Sebastien Arbogast (15)

Feedback
FeedbackFeedback
Feedback
 
Digital Hygiene for Nomads
Digital Hygiene for NomadsDigital Hygiene for Nomads
Digital Hygiene for Nomads
 
Intro to NLP
Intro to NLPIntro to NLP
Intro to NLP
 
How Chains Will Set Us Free v2
How Chains Will Set Us Free v2How Chains Will Set Us Free v2
How Chains Will Set Us Free v2
 
The Power of Gamification
The Power of GamificationThe Power of Gamification
The Power of Gamification
 
Pitch 101
Pitch 101Pitch 101
Pitch 101
 
Ethereum
EthereumEthereum
Ethereum
 
Bitcoin 101
Bitcoin 101Bitcoin 101
Bitcoin 101
 
Le progrès exponentiel
Le progrès exponentielLe progrès exponentiel
Le progrès exponentiel
 
Ethereum for visionary dummies
Ethereum for visionary dummiesEthereum for visionary dummies
Ethereum for visionary dummies
 
Pourquoi la blockchain?
Pourquoi la blockchain?Pourquoi la blockchain?
Pourquoi la blockchain?
 
Lean Startup for Developers - Devoxx Poland
Lean Startup for Developers - Devoxx PolandLean Startup for Developers - Devoxx Poland
Lean Startup for Developers - Devoxx Poland
 
La vie de développeur
La vie de développeurLa vie de développeur
La vie de développeur
 
Lean startup for MBA students
Lean startup for MBA studentsLean startup for MBA students
Lean startup for MBA students
 
Lean startup for developers
Lean startup for developersLean startup for developers
Lean startup for developers
 

Dernier

Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Mattias Andersson
 
Developer Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLDeveloper Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLScyllaDB
 
unit 4 immunoblotting technique complete.pptx
unit 4 immunoblotting technique complete.pptxunit 4 immunoblotting technique complete.pptx
unit 4 immunoblotting technique complete.pptxBkGupta21
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsPixlogix Infotech
 
The State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxThe State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxLoriGlavin3
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationSlibray Presentation
 
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxA Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxLoriGlavin3
 
SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024Lorenzo Miniero
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebUiPathCommunity
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxLoriGlavin3
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxLoriGlavin3
 
How to write a Business Continuity Plan
How to write a Business Continuity PlanHow to write a Business Continuity Plan
How to write a Business Continuity PlanDatabarracks
 
TrustArc Webinar - How to Build Consumer Trust Through Data Privacy
TrustArc Webinar - How to Build Consumer Trust Through Data PrivacyTrustArc Webinar - How to Build Consumer Trust Through Data Privacy
TrustArc Webinar - How to Build Consumer Trust Through Data PrivacyTrustArc
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brandgvaughan
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxLoriGlavin3
 
Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Commit University
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenHervé Boutemy
 
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024BookNet Canada
 

Dernier (20)

Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?Are Multi-Cloud and Serverless Good or Bad?
Are Multi-Cloud and Serverless Good or Bad?
 
Developer Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQLDeveloper Data Modeling Mistakes: From Postgres to NoSQL
Developer Data Modeling Mistakes: From Postgres to NoSQL
 
unit 4 immunoblotting technique complete.pptx
unit 4 immunoblotting technique complete.pptxunit 4 immunoblotting technique complete.pptx
unit 4 immunoblotting technique complete.pptx
 
The Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and ConsThe Ultimate Guide to Choosing WordPress Pros and Cons
The Ultimate Guide to Choosing WordPress Pros and Cons
 
The State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptxThe State of Passkeys with FIDO Alliance.pptx
The State of Passkeys with FIDO Alliance.pptx
 
Connect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck PresentationConnect Wave/ connectwave Pitch Deck Presentation
Connect Wave/ connectwave Pitch Deck Presentation
 
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptxA Deep Dive on Passkeys: FIDO Paris Seminar.pptx
A Deep Dive on Passkeys: FIDO Paris Seminar.pptx
 
SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024SIP trunking in Janus @ Kamailio World 2024
SIP trunking in Janus @ Kamailio World 2024
 
Dev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio WebDev Dives: Streamline document processing with UiPath Studio Web
Dev Dives: Streamline document processing with UiPath Studio Web
 
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptxUse of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
Use of FIDO in the Payments and Identity Landscape: FIDO Paris Seminar.pptx
 
DMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special EditionDMCC Future of Trade Web3 - Special Edition
DMCC Future of Trade Web3 - Special Edition
 
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptxThe Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
The Role of FIDO in a Cyber Secure Netherlands: FIDO Paris Seminar.pptx
 
How to write a Business Continuity Plan
How to write a Business Continuity PlanHow to write a Business Continuity Plan
How to write a Business Continuity Plan
 
TrustArc Webinar - How to Build Consumer Trust Through Data Privacy
TrustArc Webinar - How to Build Consumer Trust Through Data PrivacyTrustArc Webinar - How to Build Consumer Trust Through Data Privacy
TrustArc Webinar - How to Build Consumer Trust Through Data Privacy
 
WordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your BrandWordPress Websites for Engineers: Elevate Your Brand
WordPress Websites for Engineers: Elevate Your Brand
 
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptxMerck Moving Beyond Passwords: FIDO Paris Seminar.pptx
Merck Moving Beyond Passwords: FIDO Paris Seminar.pptx
 
Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!Nell’iperspazio con Rocket: il Framework Web di Rust!
Nell’iperspazio con Rocket: il Framework Web di Rust!
 
DevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache MavenDevoxxFR 2024 Reproducible Builds with Apache Maven
DevoxxFR 2024 Reproducible Builds with Apache Maven
 
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
Transcript: New from BookNet Canada for 2024: BNC CataList - Tech Forum 2024
 

Apps on your Wrist

  • 1. @sarbogast @eloudsa#Devoxx #smartvoxx Apps On Your Wrist Sébastien Arbogast Said Eloudrhiri
  • 2. #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
  • 3. #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
  • 4. #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
  • 5. #Devoxx #smartvoxx @sarbogast @eloudsa Disclaimer We 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?
  • 6. #Devoxx #smartvoxx @sarbogast @eloudsa Once upon a time …
  • 7. #Devoxx #smartvoxx @sarbogast @eloudsa Polex (1000 BC)
  • 8. #Devoxx #smartvoxx @sarbogast @eloudsa Pulsar P1 (70’s)
  • 9. #Devoxx #smartvoxx @sarbogast @eloudsa Casio Databank (80’s)
  • 10. #Devoxx #smartvoxx @sarbogast @eloudsa Linux wristwatch (90’s)
  • 11. #Devoxx #smartvoxx @sarbogast @eloudsa TikTok Lunatik (2011)
  • 12. #Devoxx #smartvoxx @sarbogast @eloudsa Pebble (2012)
  • 13. #Devoxx #smartvoxx @sarbogast @eloudsa Samsung Galaxy Gear (2013)
  • 14. #Devoxx #smartvoxx @sarbogast @eloudsa Moto 360 (2014)
  • 15. #Devoxx #smartvoxx @sarbogast @eloudsa Apple Watch (2015)
  • 16. #Devoxx #smartvoxx @sarbogast @eloudsa Why develop for smartwatches?
  • 17. #Devoxx #smartvoxx @sarbogast @eloudsa Glanceable
  • 18. #Devoxx #smartvoxx @sarbogast @eloudsa Sensors
  • 19. #Devoxx #smartvoxx @sarbogast @eloudsa Notification-driven
  • 20. #Devoxx #smartvoxx @sarbogast @eloudsa Small screen
  • 21. #Devoxx #smartvoxx @sarbogast @eloudsa Interactions
  • 22. #Devoxx #smartvoxx @sarbogast @eloudsa Personal use
  • 23. #Devoxx #smartvoxx @sarbogast @eloudsa Landscape Apple 
 Watch Android 
 Wear Pebble Tizen
  • 24. #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
  • 25. #Devoxx #smartvoxx @sarbogast @eloudsa • Form factors • Kinds of applications • Design guidelines Design constraints on Android Wear
  • 26. #Devoxx #smartvoxx @sarbogast @eloudsa Fragmentation: Welcome!
  • 27. #Devoxx #smartvoxx @sarbogast @eloudsa Square Round Round Chin Design constraints on Android Wear
  • 28. #Devoxx #smartvoxx @sarbogast @eloudsa • Suggest: Context Stream UI/UX Principles The right information at the right time.
  • 29. #Devoxx #smartvoxx @sarbogast @eloudsa • Demand: Cue Cards UI/UX Principles No suggestions? Just ask!
  • 30. #Devoxx #smartvoxx @sarbogast @eloudsa • Contextually aware and smart UI/UX Principles
  • 31. #Devoxx #smartvoxx @sarbogast @eloudsa • Cards Applications: Notifications
  • 32. #Devoxx #smartvoxx @sarbogast @eloudsa • Bridged notifications: natively supported Applications: Notifications
  • 33. #Devoxx #smartvoxx @sarbogast @eloudsa 4 • Custom notifications Applications: Notifications
  • 34. #Devoxx #smartvoxx @sarbogast @eloudsa Applications: Full-screen
  • 35. #Devoxx #smartvoxx @sarbogast @eloudsa Applications: Watch faces
  • 36. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx • smartvoxx.com
  • 37. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx
  • 38. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx
  • 39. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx
  • 40. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx
  • 41. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx
  • 42. #Devoxx #smartvoxx @sarbogast @eloudsa Step 1: Hello Devoxx!
  • 43. #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
  • 44. #Devoxx #smartvoxx @sarbogast @eloudsa • Mobile running Android 4.3 (API 18) or higher • Watch running Android 5.0 (API 20) or higher Requirements
  • 45. #Devoxx #smartvoxx @sarbogast @eloudsa Requirements g.co/WearCheck
  • 46. #Devoxx #smartvoxx @sarbogast @eloudsa http://tools.android.com/download/studio/stable Android Studio
  • 47. #Devoxx #smartvoxx @sarbogast @eloudsa Android SDK Manager
  • 48. #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
  • 49. #Devoxx #smartvoxx @sarbogast @eloudsa • Extras: • Android Support Repository • Android Support Library • Google Play Services • Google Repository Required libs
  • 50. #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
  • 52. #Devoxx #smartvoxx @sarbogast @eloudsa Create the projet
  • 58. #Devoxx #smartvoxx @sarbogast @eloudsa Two modules: mobile + wear
  • 59. #Devoxx #smartvoxx @sarbogast @eloudsa Step 1: Development environment
  • 60. #Devoxx #smartvoxx @sarbogast @eloudsa Create wear emulators
  • 65. #Devoxx #smartvoxx @sarbogast @eloudsa Emulator per form factor
  • 66. #Devoxx #smartvoxx @sarbogast @eloudsa • Run the Wear module: • Choose an emulator: Run on Wear Emulator
  • 67. #Devoxx #smartvoxx @sarbogast @eloudsa Square Round Round Chin Step 1: Done!
  • 68. #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
  • 74. #Devoxx #smartvoxx @sarbogast @eloudsa Step 1: Done!
  • 75. #Devoxx #smartvoxx @sarbogast @eloudsa Step 2: Devoxx CFP API • Schedules • Slots • BreakSlot • TalkSlot • Speakers
  • 76. #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
 },
 …
 ]
 } Schedules http://cfp.devoxx.be/api/conferences/DV15/schedules/
  • 77. #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" } }, … } ]} Slot http://cfp.devoxx.be/api/conferences/DV15/schedules/monday/
  • 78. #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
  • 79. #Devoxx #smartvoxx @sarbogast @eloudsa Step 2: Done!
  • 80. #Devoxx #smartvoxx @sarbogast @eloudsa Step 3: Get Schedules … monday - Monday, 9th November 2015 … tuesday - Tuesday, 10th November 2015 … wednesday - Wednesday, 11th November 2015 … thursday - Thursday, 12th November 2015 … friday - Friday, 13th November 2015
  • 81. #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
  • 82. #Devoxx #smartvoxx @sarbogast @eloudsa Google Play Services Google Play Services
  • 83. #Devoxx #smartvoxx @sarbogast @eloudsa Data Layer API Google Play Services Data Layer Message API Data API Node API
  • 84. #Devoxx #smartvoxx @sarbogast @eloudsa Google API Client Device Google Play Services Your App Google API Client Google Play services library Message API Data API Node API
  • 85. #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
  • 86. #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
  • 87. #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.
  • 88. #Devoxx #smartvoxx @sarbogast @eloudsa Watch MessageApi sendMessage() Data Layer Phone WearableListenerService onMessageReceived() Data Layer Bluetooth/Wi-Fi MessageEvent getData() getPath() Data Item Message API
  • 89. #Devoxx #smartvoxx @sarbogast @eloudsa Wi-Fi Fallback solution when Bluetooth not available. Unable to connect on remote servers.
  • 90. #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
  • 91. #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
  • 92. #Devoxx #smartvoxx @sarbogast @eloudsa <Button
 android:id="@+id/getSchedules"
 style="?android:attr/buttonStyleSmall"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout_gravity="center_horizontal"
 android:text="Get Schedules" /> Layout: Add a button
  • 93. #Devoxx #smartvoxx @sarbogast @eloudsa public class ScheduleActivity extends Activity { private GoogleApiClient mApiClient; … @Override
 protected void onStart() {
 super.onStart();
 
 mApiClient = new GoogleApiClient.Builder(this)
 .addApi(Wearable.API)
 .build();
 
 mApiClient.connect();
 }
 
 
 @Override
 protected void onStop() {
 if ((mApiClient != null) && (mApiClient.isConnected())) {
 mApiClient.disconnect();
 }
 
 super.onStop();
 } Activity: Google API Client
  • 94. #Devoxx #smartvoxx @sarbogast @eloudsa public class ScheduleActivity extends Activity { private final String SCHEDULES_PATH = "/schedules"; … @Override
 protected void onCreate(Bundle savedInstanceState) { … stub.setOnLayoutInflatedListener(new WatchViewStub.OnLayoutInflatedListener() {
 @Override
 public void onLayoutInflated(WatchViewStub stub) {
 mTextView = (TextView) stub.findViewById(R.id.text);
 
 stub.findViewById(R.id.getSchedules).setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 sendMessage(SCHEDULES_PATH, "dummy");
 }
 });
 }
 }); }
 Activity: Button Listener
  • 95. #Devoxx #smartvoxx @sarbogast @eloudsa public class ScheduleActivity extends Activity { … private void sendMessage(final String path, final String message) {
 new Thread(new Runnable() {
 @Override
 public void run() {
 // broadcast the message to all connected devices
 final NodeApi.GetConnectedNodesResult nodes = Wearable.NodeApi.getConnectedNodes(mApiClient).await();
 for (Node node : nodes.getNodes()) {
 Wearable.MessageApi.sendMessage(mApiClient, node.getId(), path, message.getBytes()).await();
 
 }
 }
 }).start();
 } Activity: Send Message
  • 96. #Devoxx #smartvoxx @sarbogast @eloudsa … dependencies { compile fileTree(dir: 'libs', include: ['*.jar'])
 wearApp project(':wear')
 compile 'com.android.support:appcompat-v7:22.2.1' compile ‘com.google.android.gms:play-services:8.1.0’ // Rest API Client
 compile ‘com.squareup.retrofit:retrofit:1.9.0’ } REST API Client library: Retrofit build.gradle
  • 97. #Devoxx #smartvoxx @sarbogast @eloudsa package net.noratek.smartvoxxwear.rest.model;
 
 import java.util.List;
 
 
 public class Schedules {
 
 private List<Link> links;
 
 public List<Link> getLinks() {
 return links;
 }
 }
 Model: Schedules
  • 98. #Devoxx #smartvoxx @sarbogast @eloudsa package net.noratek.smartvoxxwear.rest.model;
 
 /**
 * Created by eloudsa on 06/09/15.
 */
 public class Link {
 
 private String href;
 private String rel;
 private String title;
 
 
 public Link(String href, String rel, String title) {
 this.href = href;
 this.rel = rel;
 this.title = title;
 }
 
 // Getters and Setters
 … }
 Model: Link
  • 99. #Devoxx #smartvoxx @sarbogast @eloudsa package net.noratek.smartvoxxwear.rest.service;
 
 import net.noratek.smartvoxxwear.rest.model.Schedules;
 
 import retrofit.Callback;
 import retrofit.http.GET;
 import retrofit.http.Path;
 
 /**
 * Created by eloudsa on 30/10/15.
 */
 public interface DevoxxApi {
 
 
 @GET("/conferences/{conference}/schedules")
 void getSchedules(@Path("conference") String conference, Callback<Schedules> callback);
 }
 REST Endpoint for Schedules
  • 100. #Devoxx #smartvoxx @sarbogast @eloudsa WearableListenerService • New class WearService extended from WearableListenerService
  • 101. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="net.noratek.smartvoxxwear" > <uses-permission android:name="android.permission.INTERNET"/> 
 <application
 …
 <!-- Android Wear Service -->
 <service android:name=".service.WearService">
 <intent-filter>
 <action android:name="com.google.android.gms.wearable.BIND_LISTENER" />
 </intent-filter>
 </service>
 
 
 </application>
 
 </manifest>
 Service: Adapt the manifest
  • 102. #Devoxx #smartvoxx @sarbogast @eloudsa public class WearService extends WearableListenerService { … @Override
 public void onMessageReceived(MessageEvent messageEvent) {
 
 // Processing the incoming message
 String path = messageEvent.getPath();
 String data = new String(messageEvent.getData());
 
 if (path.equalsIgnoreCase(SCHEDULES_PATH)) {
 retrieveSchedules();
 return;
 }
 }
 Process “/schedules”
  • 103. #Devoxx #smartvoxx @sarbogast @eloudsa public class WearService extends WearableListenerService {
 …
 // Retrieve schedules from Devoxx
 private void retrieveSchedules() {
 // retrieve the schedules list from the server
 Callback callback = new Callback() {
 @Override
 public void success(Object o, Response response) {
 // retrieve schedule from REST
 Schedules scheduleList = (Schedules) o;
 if (scheduleList == null) {
 Log.d(TAG, "No schedules!");
 return;
 }
 
 List<Link> links = scheduleList.getLinks();
 
 for (Link link : links) {
 Log.d(TAG, Utils.getLastPartUrl(link.getHref()) + " - " + link.getTitle());
 }
 }
 
 @Override
 public void failure(RetrofitError retrofitError) {
 Log.d(TAG, retrofitError.getMessage());
 }
 };
 mMethods.getSchedules(mConferenceName, callback);
 } Retrieve Schedules
  • 104. #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
  • 105. #Devoxx #smartvoxx @sarbogast @eloudsa • Start the emulator • Start a virtual device Start Wear Emulator
  • 106. #Devoxx #smartvoxx @sarbogast @eloudsa Debugging with Emulator USB Bridge adb -d forward tcp:5601 tcp:5601
  • 107. #Devoxx #smartvoxx @sarbogast @eloudsa • Select the top right menu • Select “Pair with emulator” Pairing the emulator
  • 109. #Devoxx #smartvoxx @sarbogast @eloudsa … monday - Monday, 9th November 2015 … tuesday - Tuesday, 10th November 2015 … wednesday - Wednesday, 11th November 2015 … thursday - Thursday, 12th November 2015 … friday - Friday, 13th November 2015 Step 3: Done!
  • 110. #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)
  • 112. #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
  • 113. #Devoxx #smartvoxx @sarbogast @eloudsa Bypass transport security
  • 114. #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
  • 115. #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
  • 116. #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
  • 117. #Devoxx #smartvoxx @sarbogast @eloudsa Step 3: Done!
  • 118. #Devoxx #smartvoxx @sarbogast @eloudsa Step 4: Show Schedules
  • 119. #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
  • 120. #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
  • 121. #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
  • 122. #Devoxx #smartvoxx @sarbogast @eloudsa @Override
 protected void onResume() {
 super.onResume();
 
 // Retrieve the list of schedules
 sendMessage(SCHEDULES_PATH, "get list of schedules"); }
 Display schedules
  • 123. #Devoxx #smartvoxx @sarbogast @eloudsa public class WearService extends WearableListenerService { // send Schedules to the watch
 private void sendSchedules(List<Link> schedules) {
 final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(SCHEDULES_PATH);
 
 ArrayList<DataMap> schedulesDataMap = new ArrayList<>();
 
 // process each schedule
 for (Link schedule : schedules) {
 
 final DataMap scheduleDataMap = new DataMap();
 
 // process and push schedule's data // We need to add a timestamp to force a onDataChanged event on the remote device.
 scheduleDataMap.putString("timestamp", new Date().toString()); scheduleDataMap.putString("day", Utils.getLastPartUrl(schedule.getHref()));
 scheduleDataMap.putString("title", schedule.getTitle());
 
 schedulesDataMap.add(scheduleDataMap);
 }
 
 // store the list in the datamap to send it to the watch
 putDataMapRequest.getDataMap().putDataMapArrayList("/list", schedulesDataMap);
 
 // send the list
 if (mApiClient.isConnected()) {
 Wearable.DataApi.putDataItem(mApiClient, putDataMapRequest.asPutDataRequest());
 }
 } Send Schedules 1 2 3 5 6 4
  • 124. #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
  • 125. #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
  • 126. #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
  • 127. #Devoxx #smartvoxx @sarbogast @eloudsa Layout: Rect
  • 128. #Devoxx #smartvoxx @sarbogast @eloudsa @Override
 protected void onCreate(Bundle savedInstanceState) {
 …
 // Listview component
 listView = (WearableListView) findViewById(R.id.wearable_list);
 
 // Assign the adapter
 listViewAdapter = new ListViewAdapter(ScheduleActivity.this, new ArrayList<Schedule>());
 listView.setAdapter(listViewAdapter);
 … }
 ListViewAdapter
  • 129. #Devoxx #smartvoxx @sarbogast @eloudsa WearableListView
  • 130. #Devoxx #smartvoxx @sarbogast @eloudsa // Inner class providing the WearableListview's adapter
 public class ListViewAdapter extends WearableListView.Adapter {
 … // Create new views for list items
 // (invoked by the WearableListView's layout manager)
 @Override
 public WearableListView.ViewHolder onCreateViewHolder(ViewGroup parent,
 int viewType) {
 // Inflate our custom layout for list items
 return new ItemViewHolder(new SettingsItemView(mContext));
 } … Animation
  • 131. #Devoxx #smartvoxx @sarbogast @eloudsa public final class SettingsItemView extends FrameLayout implements WearableListView.OnCenterProximityListener {
 
 private TextView description;
 
 public SettingsItemView(Context context) {
 super(context);
 View.inflate(context, R.layout.schedule_row_activity, this);
 
 description = (TextView) findViewById(R.id.description);
 }
 
 @Override
 public void onCenterPosition(boolean b) {
 description.animate().scaleX(1f).scaleY(1f).alpha(1);
 }
 
 @Override
 public void onNonCenterPosition(boolean b) {
 description.animate().scaleX(0.8f).scaleY(0.8f).alpha(0.6f);
 }
 } Animation
  • 132. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 …>
 
 <TextView
 android:id="@+id/title"
 …/>
 
 
 <RelativeLayout
 android:layout_width="fill_parent"
 android:layout_height="fill_parent">
 
 <android.support.wearable.view.WearableListView
 android:id="@+id/wearable_list"
 …>
 </android.support.wearable.view.WearableListView>
 
 <ProgressBar
 android:id="@+id/progressBar"
 … />
 
 </RelativeLayout>
 </LinearLayout> Layout: schedule_rect_activity 1 2 3 4
  • 133. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
 
 
 <TextView
 android:id="@+id/description"
 …/>
 
 </merge>
 
 Layout: schedule_row_activity
  • 134. #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
  • 136. #Devoxx #smartvoxx @sarbogast @eloudsa Step 4: Done!
  • 137. #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)
  • 139. #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
  • 140. #Devoxx #smartvoxx @sarbogast @eloudsa Step 4: Show Schedules • Call Devoxx loading method in willActivate and initialize table • Remove Devoxx call from ExtensionDelegate • RUN!
  • 141. #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
  • 142. #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
  • 143. #Devoxx #smartvoxx @sarbogast @eloudsa Add WKInterfaceTable to view
  • 144. #Devoxx #smartvoxx @sarbogast @eloudsa Identify row controller
  • 145. #Devoxx #smartvoxx @sarbogast @eloudsa import WatchKit class ScheduleRowController: NSObject { } ScheduleRowController class
  • 146. #Devoxx #smartvoxx @sarbogast @eloudsa Label outlet in row controller
  • 147. #Devoxx #smartvoxx @sarbogast @eloudsa Table outlet in interface controller
  • 148. #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
  • 149. #Devoxx #smartvoxx @sarbogast @eloudsa Step 4: Done!
  • 150. #Devoxx #smartvoxx @sarbogast @eloudsa Step 5: Select a Schedule
  • 151. #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
  • 152. #Devoxx #smartvoxx @sarbogast @eloudsa @Override
 public void onBindViewHolder(WearableListView.ViewHolder holder,
 int position) {
 
 // retrieve the text view
 ItemViewHolder itemHolder = (ItemViewHolder) holder;
 TextView view = itemHolder.textView;
 
 // retrieve, transform and display the schedule's day
 Schedule schedule = mDataset.get(position);
 String scheduleDay = schedule.getTitle().replace(",", "n");
 view.setText(scheduleDay);
 
 // replace list item's metadata
 holder.itemView.setTag(schedule);
 }
 Store Schedule’s data 1 2
  • 153. #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
  • 154. #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
  • 155. #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
  • 156. #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
  • 158. #Devoxx #smartvoxx @sarbogast @eloudsa Step 5: Done!
  • 159. #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
  • 160. #Devoxx #smartvoxx @sarbogast @eloudsa Add ScheduleInterfaceController
  • 161. #Devoxx #smartvoxx @sarbogast @eloudsa Give identifier to push segue
  • 162. #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
  • 163. #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
  • 164. #Devoxx #smartvoxx @sarbogast @eloudsa Step 5: Done!
  • 165. #Devoxx #smartvoxx @sarbogast @eloudsa Step 6: Get Slots
  • 166. #Devoxx #smartvoxx @sarbogast @eloudsa Retrieve Slots Get back on the Network. Seriously???
  • 167. #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
  • 168. #Devoxx #smartvoxx @sarbogast @eloudsa Wear Data Item (Cache) .getInt() DataMap .getDataIetms() DataApi wear://path_to_data 1 2 Fetch from local cache MessageApi sendMessage() 3 4
  • 169. #Devoxx #smartvoxx @sarbogast @eloudsa public class WearService extends WearableListenerService { // send Schedules to the watch
 private void sendSchedules(List<Link> schedules) {
 final PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(SCHEDULES_PATH);
 
 ArrayList<DataMap> schedulesDataMap = new ArrayList<>();
 
 // process each schedule
 for (Link schedule : schedules) {
 
 final DataMap scheduleDataMap = new DataMap();
 
 // process and push schedule's data
 scheduleDataMap.putString("day", Utils.getLastPartUrl(schedule.getHref()));
 scheduleDataMap.putString("title", schedule.getTitle());
 
 schedulesDataMap.add(scheduleDataMap);
 }
 
 // store the list in the datamap to send it to the watch
 putDataMapRequest.getDataMap().putDataMapArrayList("/list", schedulesDataMap);
 
 // send the list
 if (mApiClient.isConnected()) {
 Wearable.DataApi.putDataItem(mApiClient, putDataMapRequest.asPutDataRequest());
 }
 } Changes on Phone: No timestamp 1
  • 170. #Devoxx #smartvoxx @sarbogast @eloudsa @Override
 protected void onResume() {
 super.onResume();
 
 // Retrieve and display the list of schedules
 getSchedules(SCHEDULES_PATH);
 }
 Display data items
  • 171. #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
  • 173. #Devoxx #smartvoxx @sarbogast @eloudsa Step 6: Done!
  • 174. #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
  • 175. #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
  • 176. #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
  • 177. #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
  • 178. #Devoxx #smartvoxx @sarbogast @eloudsa Link with CoreData framework
  • 179. #Devoxx #smartvoxx @sarbogast @eloudsa Create object model
  • 180. #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
  • 181. #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
  • 182. #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
  • 183. #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
  • 184. #Devoxx #smartvoxx @sarbogast @eloudsa Model Conference and Schedule
  • 185. #Devoxx #smartvoxx @sarbogast @eloudsa Model Conference and Schedule
  • 186. #Devoxx #smartvoxx @sarbogast @eloudsa Generate NSManagedObject subs
  • 187. #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…
  • 188. #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…
  • 189. #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…
  • 190. #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…
  • 191. #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…
  • 192. #Devoxx #smartvoxx @sarbogast @eloudsa Step 6: Done!
  • 193. #Devoxx #smartvoxx @sarbogast @eloudsa Step 7: Get Talk
  • 194. #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)
  • 195. #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
  • 196. #Devoxx #smartvoxx @sarbogast @eloudsa Fragment Fragment Fragment Fragment • TalkFragment • TalkSummaryFragment • TalkSpeakerFragment FragmentGridPagerAdapter TalkActivity
  • 197. #Devoxx #smartvoxx @sarbogast @eloudsa • Define the layout • Create the fragments • Create the Adapter • Link the Adapter Create the GridViewPager
  • 198. #Devoxx #smartvoxx @sarbogast @eloudsa <FrameLayout xmlns: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.GridViewPager xmlns: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>
 Layout res/layout/talk_activity.xml
  • 199. #Devoxx #smartvoxx @sarbogast @eloudsa Talk detail: Custom layout
  • 200. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="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> Layout res/layout/talk_fragment.xml
  • 201. #Devoxx #smartvoxx @sarbogast @eloudsa <RelativeLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="fill_parent"
 android:layout_height="fill_parent"
 android:paddingTop="15dp">
 
 <TextView
 android:id="@+id/title"
 android:layout_width="fill_parent"
 …. Layout res/layout/talk_rect_fragment.xml
  • 202. #Devoxx #smartvoxx @sarbogast @eloudsa <android.support.wearable.view.BoxInsetLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:padding="15dp">
 
 <RelativeLayout
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 
 <TextView
 android:id="@+id/title"
 Layout res/layout/talk_round_fragment.xml
  • 203. #Devoxx #smartvoxx @sarbogast @eloudsa Summary: Card
  • 204. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <android.support.wearable.view.CardScrollView xmlns: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.CardFrame
 android:id="@+id/card_frame"
 …>
 
 <LinearLayout
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:orientation="vertical">
 
 <TextView
 android:id="@+id/title"
 Summary: Layout
  • 205. #Devoxx #smartvoxx @sarbogast @eloudsa Summary: Expand content
  • 206. #Devoxx #smartvoxx @sarbogast @eloudsa public class TalkSummaryFragment extends Fragment { … description.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 if (mTalkSummary == null) {
 return;
 }
 
 if (mEllipsize) {
 description.setText(mTalkSummary);
 } else {
 description.setText(StringUtils.abbreviate(mTalkSummary, ELLIPSIS_SIZE));
 }
 mEllipsize = !mEllipsize; 
 // Force the CardScrollView to reset to its initial position
 mainView.findViewById(R.id.card_scroll_view).setScrollX(0);
 mainView.findViewById(R.id.card_scroll_view).setScrollY(0);
 
 }
 });
 Summary: Expand content
  • 207. #Devoxx #smartvoxx @sarbogast @eloudsa TalkActivity Events Fragments Events
  • 208. #Devoxx #smartvoxx @sarbogast @eloudsa EventBus (BusWear)
  • 209. #Devoxx #smartvoxx @sarbogast @eloudsa EventBus (BusWear)
  • 210. #Devoxx #smartvoxx @sarbogast @eloudsa dependencies {
 compile fileTree(dir: 'libs', include: ['*.jar'])
 compile 'com.google.android.support:wearable:1.3.0'
 compile 'com.google.android.gms:play-services-wearable:8.1.0'
 
 // Event Bus: BusWear
 compile 'pl.tajchert:buswear:0.9.5'
 
 …
 } EventBus build.grade
  • 211. #Devoxx #smartvoxx @sarbogast @eloudsa EventBus.getDefault().postLocal(new TalkEvent(mTalk)); EventBus - Sending events
  • 212. #Devoxx #smartvoxx @sarbogast @eloudsa public void onEvent(AddFavoriteEvent addFavoritesEvent) {
 … } EventBus - Receive events Required to register/unregister to the bus
  • 213. #Devoxx #smartvoxx @sarbogast @eloudsa public class TalkActivity extends Activity … { @Override
 protected void onStart() {
 …
 EventBus.getDefault().register(this);
 }
 
 @Override
 protected void onStop() {
 EventBus.getDefault().unregister(this);
 … } EventBus - (Un)Register
  • 214. #Devoxx #smartvoxx @sarbogast @eloudsa Twitter: Confirmation animation
  • 215. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="net.noratek.smartvoxxwear"> … <activity android:name="android.support.wearable.activity.ConfirmationActivity" /> … Confirmation animation Update the manifest of the watch
  • 216. #Devoxx #smartvoxx @sarbogast @eloudsa @Override
 public View onCreateView(LayoutInflater inflater, ViewGroup container,
 Bundle savedInstanceState) { mainView.findViewById(R.id.twitterIcon).setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v) {
 startConfirmationActivity(ConfirmationActivity.OPEN_ON_PHONE_ANIMATION, getString(R.string.confirmation_open_on_phone)); 
 EventBus.getDefault().postLocal(new ConfirmationEvent(TWITTER_PATH, (String) mainView.findViewById(R.id.twitterIcon).getTag()));
 }
 }); Confirmation animation (Listener) 1 2
  • 217. #Devoxx #smartvoxx @sarbogast @eloudsa private void startConfirmationActivity(int animationType, String message) { 
 Intent confirmationActivity = new Intent(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
  • 218. #Devoxx #smartvoxx @sarbogast @eloudsa public class WearService extends WearableListenerService { … private void followOnTwitter(String inputData) {
 String twitterName = inputData == null ? "" : inputData.trim().toLowerCase();
 twitterName = twitterName.replaceFirst("@", "");
 if (twitterName.isEmpty()) {
 return;
 }
 
 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://twitter.com/" + twitterName));
 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
 this.startActivity(intent);
 } } Open Twitter on Phone
  • 219. #Devoxx #smartvoxx @sarbogast @eloudsa Step 7: Square GridViewPager CardCustom Custom Custom WearableListView Step 7: Done!
  • 220. #Devoxx #smartvoxx @sarbogast @eloudsa Step 7: Round CardCustom Custom Custom WearableListView GridViewPager
  • 221. #Devoxx #smartvoxx @sarbogast @eloudsa Step 7: Get, Cache and Show Talk • Time travel
  • 222. #Devoxx #smartvoxx @sarbogast @eloudsa Step 7: Done!
  • 223. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Favorites
  • 224. #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
  • 225. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Favorites Phone Watch 1 2 3 4 eventId remove read add Calendar remove read add
  • 226. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Reminders Phone 1 Calendar add AlarmManager 2 10 minutes before
  • 227. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Alarm Service Phone 1 Calendar add AlarmManager 2 10 minutes before AlarmService talkId, title, eventId, schedule, … 3
  • 228. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Wake-up Phone AlarmManager AlarmService talkId, title, eventId, schedule, …1 AlarmReceiver talkId, title, eventId, schedule, … .broadcast() 2
  • 229. #Devoxx #smartvoxx @sarbogast @eloudsa <?xml version="1.0" encoding="utf-8"?>
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="net.noratek.smartvoxxwear" >
 
 <!-- Add, Read and remove favorites on the calendar -->
 <uses-permission android:name="android.permission.READ_CALENDAR" />
 <uses-permission android:name="android.permission.WRITE_CALENDAR" />
 Calendar: Add permissions Update the manifest of the phone
  • 230. #Devoxx #smartvoxx @sarbogast @eloudsa <application> … <service android:name=".alarm.AlarmService"/>
 
 <receiver android:name=".alarm.AlarmReceiver">
 <intent-filter>
 <action android:name="net.noratek.smartvoxxwear.AlarmService.BROADCAST" />
 </intent-filter>
 </receiver> </application Alarm: Service, Receiver Update the manifest of the phone
  • 231. #Devoxx #smartvoxx @sarbogast @eloudsa Add: Tap on favorite icon
  • 232. #Devoxx #smartvoxx @sarbogast @eloudsa Mobile: event on Calendar • Title • Room • Summary • Schedule
  • 233. #Devoxx #smartvoxx @sarbogast @eloudsa Watch: Confirmation received
  • 234. #Devoxx #smartvoxx @sarbogast @eloudsa Remove: Tap on favorite icon
  • 235. #Devoxx #smartvoxx @sarbogast @eloudsa Event removed from Calendar
  • 237. #Devoxx #smartvoxx @sarbogast @eloudsa Stop 8: Done!
  • 238. #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
  • 239. #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
  • 240. #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
  • 241. #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
  • 242. #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
  • 243. #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
  • 244. #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…
  • 245. #Devoxx #smartvoxx @sarbogast @eloudsa Step 8: Done!
  • 246. #Devoxx #smartvoxx @sarbogast @eloudsa Step 9: Custom notifications
  • 247. #Devoxx #smartvoxx @sarbogast @eloudsa Phone: Prepare and Send Phone AlarmManager AlarmReceiver Custom notification Action EventService 3 4 AlarmService talkId, title, eventId, schedule, …1 .broadcast() 2
  • 248. #Devoxx #smartvoxx @sarbogast @eloudsa // Create an intent for the reply action
 Intent actionIntent = new Intent(context, EventService.class);
 actionIntent.putExtras(bundle);
 PendingIntent actionPendingIntent =
 PendingIntent.getService(context, 0, actionIntent,
 PendingIntent.FLAG_UPDATE_CURRENT);
 
 // Create the action
 NotificationCompat.Action action =
 new NotificationCompat.Action.Builder(R.drawable.ic_calendar,
 context.getText(R.string.remove_event), actionPendingIntent)
 .build(); Notification: Action to Service
  • 249. #Devoxx #smartvoxx @sarbogast @eloudsa // Add a notification with the same action on mobile and watch
 NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
 .setSmallIcon(R.drawable.ic_logo)
 .setContentTitle(talk.getTitle())
 .setContentText(information)
 .setAutoCancel(true)
 .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000, 1000})
 .setDefaults(Notification.DEFAULT_ALL)
 .addAction(action);
 NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(mBuilder.build());
 
 Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_black);
 wearableExtender.setBackground(bitmap);
 
 wearableExtender.extend(mBuilder);
 
 NotificationManagerCompat manager = NotificationManagerCompat.from(context);
 manager.notify(notificationId, mBuilder.build()); Notification: Builder
  • 250. #Devoxx #smartvoxx @sarbogast @eloudsa Receiving notification
  • 251. #Devoxx #smartvoxx @sarbogast @eloudsa // Add a notification with an action only visible on the watch
 NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(context)
 .setSmallIcon(R.drawable.ic_logo)
 .setContentTitle(talk.getTitle())
 .setContentText(information)
 .setAutoCancel(true)
 .setVibrate(new long[]{1000, 1000, 1000, 1000, 1000, 1000})
 .setDefaults(Notification.DEFAULT_ALL)
 .extend(new NotificationCompat.WearableExtender().addAction(action)); NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(mBuilder.build());
 
 Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.ic_black);
 wearableExtender.setBackground(bitmap);
 
 wearableExtender.extend(mBuilder);
 
 NotificationManagerCompat manager = NotificationManagerCompat.from(context);
 manager.notify(notificationId, mBuilder.build()); Wearable-only action
  • 252. #Devoxx #smartvoxx @sarbogast @eloudsa Wearable-only Wearable-only action
  • 253. #Devoxx #smartvoxx @sarbogast @eloudsa Watch: Action Remove Phone-> EventService
  • 254. #Devoxx #smartvoxx @sarbogast @eloudsa Phone: EventService Phone Calendar EventService remove Event 2 1 3 Event removed
  • 255. #Devoxx #smartvoxx @sarbogast @eloudsa <application> … <service android:name=".service.EventService"/> </application Alarm: EventService Update the manifest of the phone
  • 256. #Devoxx #smartvoxx @sarbogast @eloudsa public class EventService extends Service { @Override
 public int onStartCommand(Intent intent, int flags, int startId) { … // remove the event from the calendar
 CalendarHelper calendarHelper = new CalendarHelper(this);
 calendarHelper.removeEvent(eventId); … sendFavorite(talkId, 0L); .. } Phone: EventService
  • 258. #Devoxx #smartvoxx @sarbogast @eloudsa Step 9: Done!
  • 259. #Devoxx #smartvoxx @sarbogast @eloudsa Step 9: Notifications • Schedule notifications on the iPhone • Custom notification controller
  • 260. #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
  • 261. #Devoxx #smartvoxx @sarbogast @eloudsa Custom notification controller
  • 262. #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
  • 263. #Devoxx #smartvoxx @sarbogast @eloudsa Step 9: Done!
  • 264. #Devoxx #smartvoxx @sarbogast @eloudsa Step 10:Glances and Complications • Glance controller • Complication controller
  • 265. #Devoxx #smartvoxx @sarbogast @eloudsa Glance controller
  • 266. #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
  • 267. #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
  • 268. #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
  • 269. #Devoxx #smartvoxx @sarbogast @eloudsa Step 10: Done!
  • 270. #Devoxx #smartvoxx @sarbogast @eloudsa Step 11: Release the App
  • 271. #Devoxx #smartvoxx @sarbogast @eloudsa Prepare the build • Include all permissions of wearable into Phone • Same package name and version number
  • 272. #Devoxx #smartvoxx @sarbogast @eloudsa Generate signed APK Mobile embeds Wear
  • 273. #Devoxx #smartvoxx @sarbogast @eloudsa Mobile APK embeds Wearable Phone App Module Code Resources Wearable App Watch App Module Code Resources
  • 274. #Devoxx #smartvoxx @sarbogast @eloudsa Publishing: Select Android Wear
  • 275. #Devoxx #smartvoxx @sarbogast @eloudsa Distribution Companion App Wearable App Bluetooth Companion App Play Services Android Wear Wearable App Smartvoxx
  • 276. #Devoxx #smartvoxx @sarbogast @eloudsa Step 11: Done!
  • 277. #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…
  • 278. #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
  • 279. #Devoxx #smartvoxx @sarbogast @eloudsa static bool load_shutter_group_list() { if(accessToken && sizeof(accessToken) > 0) { DictionaryIterator *iter; app_message_outbox_begin(&iter); if (iter == NULL) { APP_LOG(APP_LOG_LEVEL_DEBUG, "null iter"); return false; } Tuplet message_type_tuple = TupletInteger(MESSAGE_TYPE, LOAD_SHUTTER_GROUP_LIST); dict_write_tuplet(iter, &message_type_tuple); Tuplet access_token_tuple = TupletCString(ACCESS_TOKEN, accessToken); dict_write_tuplet(iter, &access_token_tuple); Tuplet refresh_token_tuple = TupletCString(REFRESH_TOKEN, refreshToken); dict_write_tuplet(iter, &refresh_token_tuple); Tuplet site_id_tuple = TupletInteger(SITE_ID, selected_site_id); dict_write_tuplet(iter, &site_id_tuple); dict_write_end(iter); app_message_outbox_send(); return true; } else { return false; } } Pebble code
  • 280. #Devoxx #smartvoxx @sarbogast @eloudsa function loadShutterGroupList(accessToken, refreshToken, args) { console.log("Loading shutter group list for access token " + accessToken + " and site " + args[0]); var response; var req = new XMLHttpRequest(); // build the GET request var url = "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 - HTTP OK if(req.status == 200) { console.log(req.responseText); response = JSON.parse(req.responseText); var shutterGroupList; if (response.status === 'OK') { shutterGroupList = response.payload.items; var msg = {}; msg.messageType = MessageType.SHUTTER_GROUP_LIST; for(var i = 0; i < shutterGroupList.length; i++){ var shutterGroup = shutterGroupList[i]; msg['' + shutterGroup.groupId] = shutterGroup.label; } console.log("Sending response back to Pebble: " + JSON.stringify(msg)); Pebble.sendAppMessage(msg); } else { console.log("Status not OK"); Pebble.sendAppMessage({messageType:MessageType.ERROR, errorMessage:"Could not load shutter groups."}); } } else if(req.status == 401 && refreshToken){ getNewAccessToken(refreshToken, loadShutterGroupList, args); } else { console.log("Request returned error code " + req.status.toString()); Pebble.sendAppMessage({messageType:MessageType.ERROR, errorMessage:"Could not load shutter groups."}); } } }; req.send(null); } Pebble code
  • 281. #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
  • 282. #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?)
  • 283. #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
  • 284. #Devoxx #smartvoxx @sarbogast @eloudsa One more thing …
  • 285. #Devoxx #smartvoxx @sarbogast @eloudsa Smartvoxx on Github Available in Black … … and White Coming soon…