apps on your wrist
TRANSCRIPT
#Devoxx #smartvoxx @sarbogast @eloudsa
• Who owns a smartwatch?
• Who is an Android developer?
• Who is an iOS developer?
• Who is a Pebble developer?
• Who is a Rolex developer?
• Who has already written a smartwatch app?
• Who is a member of the Night’s Watch?
Survey
#Devoxx #smartvoxx @sarbogast @eloudsa
Sébastien Arbogast@sarbogast
• Java developer for 10 years
• iOS developers for 5 years (developer of the first Devoxx schedule app)
• Pebble developer for 2 years
• Owner of TikTok Lunatik with iPod Nano
• VP of engineering for Take Eat Easy
#Devoxx #smartvoxx @sarbogast @eloudsa
Said Eloudrhiri @eloudsa
• Developer since 1992
• Agile Coach and trainer
• Devoxx4Kids helper (Sphero, MindStorms, CodeCombat)
• Side Projects: mobile development
• Husband and father of Nora, Rayane and Djenna
• No kitten but a dog
#Devoxx #smartvoxx @sarbogast @eloudsa
DisclaimerWe are not related to Google, Apple or Pebble.We are just curious developers sharing our experience.Materials used in this presentation remains the property of their owners.
Any questions?
#Devoxx #smartvoxx @sarbogast @eloudsa
• 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
• 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
• Bridged notifications: natively supported
Applications: Notifications
#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
http://tools.android.com/download/studio/stable
Android Studio
#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
#Devoxx #smartvoxx @sarbogast @eloudsa
• Run the Wear module:
• Choose an emulator:
Run on Wear Emulator
#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
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 2: Devoxx CFP API
• Schedules
• Slots
• BreakSlot
• TalkSlot
• Speakers
#Devoxx #smartvoxx @sarbogast @eloudsa
{ "links": [ { "href": "http://cfp.devoxx.be/api/conferences/DV15/schedules/monday/", "rel": "http://cfp.devoxx.be/api/profile/schedule", "title": "Monday, 9th November 2015 }, … ]}
Scheduleshttp://cfp.devoxx.be/api/conferences/DV15/schedules/
#Devoxx #smartvoxx @sarbogast @eloudsa
{"slots": [ { "roomId": "a_hall", "notAllocated": false, "fromTimeMillis": 1447052400000, "break": { "id": "reg", "nameEN": "Registration, Welcome and Breakfast", "nameFR": "Accueil", "room": { "id": "a_hall", "name": "Exhibition floor", "capacity": 1500, "setup": "special" } }, … } ]}
Slothttp://cfp.devoxx.be/api/conferences/DV15/schedules/monday/
#Devoxx #smartvoxx @sarbogast @eloudsa
API for Devoxx sessions
• http://cfp.devoxx.be/api
• http://cfp.devoxx.fr/api
• http://cfp.devoxx.co.uk/api
• http://cfp.devoxx.ma/api
• http://cfp.devoxx.pl/api
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 3: Get Schedules
…monday-Monday,9thNovember2015…tuesday-Tuesday,10thNovember2015…wednesday-Wednesday,11thNovember2015…thursday-Thursday,12thNovember2015…friday-Friday,13thNovember2015
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 3: Get Schedules
• Google Play Services and the Data Layer API:
• Node API, Message API
• The watch sends a request to the phone
• The phone accesses the network to fetch the schedules
• The phone logs the schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
Data Layer API
Google Play ServicesData Layer
Message API
Data API
Node API
#Devoxx #smartvoxx @sarbogast @eloudsa
Google API ClientDevice
Google Play Services
Your App
Google API Client
Google Play services library Message API
Data API
Node API
#Devoxx #smartvoxx @sarbogast @eloudsa
Node API
• Learn more about local or connected Nodes
• Display name
• Id to identify the node in the Android Wear network
• Nearby the local node
#Devoxx #smartvoxx @sarbogast @eloudsa
Message API
• One-way communication
• Message (Data Item) sent to the connected device
• path -> identifies the message
• payload -> small message payload
#Devoxx #smartvoxx @sarbogast @eloudsa
Data Item
Path Payload
/path/to/your/data Byte array max: 100 Kb
Asset (Binary Blob) can go beyond this limitation of 100Kb.Requires the Data API.
#Devoxx #smartvoxx @sarbogast @eloudsa
Watch
MessageApi
sendMessage()
Data Layer
Phone
WearableListenerService
onMessageReceived()
Data LayerBluetooth/Wi-Fi
MessageEvent
getData()getPath()
Data Item
Message API
#Devoxx #smartvoxx @sarbogast @eloudsa
Wi-Fi
Fallback solution when Bluetooth not available.
Unable to connect on remote servers.
#Devoxx #smartvoxx @sarbogast @eloudsa
Watch part
• Declare the Google API Client
• Connect-Disconnect to-from Google Play Services
• Define the message path
• Add a button listener
• Send the message to the phone to get the schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
Phone part• WearableListenerService: receive events from the Data Layer
• Process message path “/schedules” (onMessageReceived)
• Retrieves schedules with Retrofit (REST API Client)
• Logs schedules on the console
#Devoxx #smartvoxx @sarbogast @eloudsa
<Buttonandroid:id="@+id/getSchedules"style="?android:attr/buttonStyleSmall"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_gravity="center_horizontal" android:text="GetSchedules"/>
Layout: Add a button
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassScheduleActivityextendsActivity{
privateGoogleApiClientmApiClient;
…
@OverrideprotectedvoidonStart(){super.onStart();mApiClient=newGoogleApiClient.Builder(this) .addApi(Wearable.API).build();mApiClient.connect();}@OverrideprotectedvoidonStop(){if((mApiClient!=null)&&(mApiClient.isConnected())){ mApiClient.disconnect();}super.onStop();}
Activity: Google API Client
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassScheduleActivityextendsActivity{
privatefinalStringSCHEDULES_PATH="/schedules";
…
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){
…stub.setOnLayoutInflatedListener(newWatchViewStub.OnLayoutInflatedListener(){ @OverridepublicvoidonLayoutInflated(WatchViewStubstub){ mTextView=(TextView)stub.findViewById(R.id.text); stub.findViewById(R.id.getSchedules).setOnClickListener(newView.OnClickListener(){ @OverridepublicvoidonClick(Viewv){sendMessage(SCHEDULES_PATH,"dummy"); }});}});
}
Activity: Button Listener
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassScheduleActivityextendsActivity{
…
privatevoidsendMessage(finalStringpath,finalStringmessage){ newThread(newRunnable(){@Overridepublicvoidrun(){//broadcastthemessagetoallconnecteddevices finalNodeApi.GetConnectedNodesResultnodes=Wearable.NodeApi.getConnectedNodes(mApiClient).await(); for(Nodenode:nodes.getNodes()){ Wearable.MessageApi.sendMessage(mApiClient,node.getId(),path,message.getBytes()).await(); }}}).start();}
Activity: Send Message
#Devoxx #smartvoxx @sarbogast @eloudsa
…
dependencies{compilefileTree(dir:'libs',include:['*.jar']) wearAppproject(':wear')compile'com.android.support:appcompat-v7:22.2.1'compile‘com.google.android.gms:play-services:8.1.0’//RestAPIClientcompile‘com.squareup.retrofit:retrofit:1.9.0’
}
REST API Client library: Retrofit build.gradle
#Devoxx #smartvoxx @sarbogast @eloudsa
packagenet.noratek.smartvoxxwear.rest.model;importjava.util.List;publicclassSchedules{privateList<Link>links;publicList<Link>getLinks(){returnlinks;}}
Model: Schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
packagenet.noratek.smartvoxxwear.rest.model; /***Createdbyeloudsaon06/09/15.*/publicclassLink{privateStringhref;privateStringrel;privateStringtitle;publicLink(Stringhref,Stringrel,Stringtitle){ this.href=href;this.rel=rel;this.title=title;}//GettersandSetters…}
Model: Link
#Devoxx #smartvoxx @sarbogast @eloudsa
packagenet.noratek.smartvoxxwear.rest.service; importnet.noratek.smartvoxxwear.rest.model.Schedules; importretrofit.Callback;importretrofit.http.GET;importretrofit.http.Path;/***Createdbyeloudsaon30/10/15.*/publicinterfaceDevoxxApi{@GET("/conferences/{conference}/schedules") voidgetSchedules(@Path("conference")Stringconference,Callback<Schedules>callback); }
REST Endpoint for Schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
WearableListenerService
• New class WearService extended from WearableListenerService
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear">
<uses-permissionandroid:name="android.permission.INTERNET"/><application…<!--AndroidWearService--><serviceandroid:name=".service.WearService"> <intent-filter><actionandroid:name="com.google.android.gms.wearable.BIND_LISTENER"/> </intent-filter></service></application></manifest>
Service: Adapt the manifest
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassWearServiceextendsWearableListenerService{
…
@OverridepublicvoidonMessageReceived(MessageEventmessageEvent){ //ProcessingtheincomingmessageStringpath=messageEvent.getPath();Stringdata=newString(messageEvent.getData()); if(path.equalsIgnoreCase(SCHEDULES_PATH)){ retrieveSchedules();return;}}
Process “/schedules”
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassWearServiceextendsWearableListenerService{ …//RetrieveschedulesfromDevoxxprivatevoidretrieveSchedules(){//retrievethescheduleslistfromtheserver Callbackcallback=newCallback(){@Overridepublicvoidsuccess(Objecto,Responseresponse){ //retrieveschedulefromRESTSchedulesscheduleList=(Schedules)o; if(scheduleList==null){Log.d(TAG,"Noschedules!");return;}List<Link>links=scheduleList.getLinks(); for(Linklink:links){Log.d(TAG,Utils.getLastPartUrl(link.getHref())+"-"+link.getTitle()); }}@Overridepublicvoidfailure(RetrofitErrorretrofitError){ Log.d(TAG,retrofitError.getMessage()); }};mMethods.getSchedules(mConferenceName,callback); }
Retrieve Schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
Run on Phone
• Build and deploy on Phone and Watch
• Forwarding ports (adb forward tcp:…):
• Link Phone to Wear Emulator
• On the Watch, tap on “GET SCHEDULES” button
• Check the log output of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
• Start the emulator
• Start a virtual device
Start Wear Emulator
#Devoxx #smartvoxx @sarbogast @eloudsa
Debugging with Emulator
USB
Bridge
adb -d forward tcp:5601 tcp:5601
#Devoxx #smartvoxx @sarbogast @eloudsa
• Select the top right menu
• Select “Pair with emulator”
Pairing the emulator
#Devoxx #smartvoxx @sarbogast @eloudsa
…monday-Monday,9thNovember2015…tuesday-Tuesday,10thNovember2015…wednesday-Wednesday,11thNovember2015…thursday-Thursday,12thNovember2015…friday-Friday,13thNovember2015
Step 3: Done!
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 3: Get Schedules
• If phone is connected, go through phone
• Otherwise access the network directly
• Transparent for the developer
• Access to the same network SDK as on the iPhone (NSURLSession)
#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
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 4: Show Schedules
• The phone sends the schedules to the watch (Data API)
• The watch receives the schedules
• The watch displays the schedules on a list view
#Devoxx #smartvoxx @sarbogast @eloudsa
Data API
• Support one-way or two-way data communication
• Sending Binary Blog (Asset)
• Synchronise data between connected devices
• Synchronise data when connection is re-established
• Data caching
#Devoxx #smartvoxx @sarbogast @eloudsa
Phone
Data Layer
Watch
Data Layer
.create(“/path”)
PutDataMapRequest
.putInt(KEY, data)
DataMap
.asPutDataRequest()
PutDataRequest
.putDataItem()
Wearable.DataApi
Data Item (Shared)
.getInt()
DataMap
.getDataItem()
DataEvent
.fromDataItem(DataItem)
DataMapItem
onDataChanged()
DataApi.DataListenerBluetooth/Wi-Fi
Data API
#Devoxx #smartvoxx @sarbogast @eloudsa
@OverrideprotectedvoidonResume(){super.onResume();//RetrievethelistofschedulessendMessage(SCHEDULES_PATH,"getlistofschedules");}
Display schedules
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassWearServiceextendsWearableListenerService{
//sendSchedulestothewatchprivatevoidsendSchedules(List<Link>schedules){ finalPutDataMapRequestputDataMapRequest=PutDataMapRequest.create(SCHEDULES_PATH); ArrayList<DataMap>schedulesDataMap=newArrayList<>(); //processeachschedulefor(Linkschedule:schedules){finalDataMapscheduleDataMap=newDataMap(); //processandpushschedule'sdata//WeneedtoaddatimestamptoforceaonDataChangedeventontheremotedevice. scheduleDataMap.putString("timestamp",newDate().toString());
scheduleDataMap.putString("day",Utils.getLastPartUrl(schedule.getHref())); scheduleDataMap.putString("title",schedule.getTitle()); schedulesDataMap.add(scheduleDataMap); }//storethelistinthedatamaptosendittothewatch putDataMapRequest.getDataMap().putDataMapArrayList("/list",schedulesDataMap); //sendthelistif(mApiClient.isConnected()){Wearable.DataApi.putDataItem(mApiClient,putDataMapRequest.asPutDataRequest()); }}
Send Schedules
1
2
3
5
6
4
#Devoxx #smartvoxx @sarbogast @eloudsa
public class ScheduleActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {
@Override protected void onStart() { super.onStart(); mApiClient = new GoogleApiClient.Builder(this) .addApi(Wearable.API) .addConnectionCallbacks(this) .build(); mApiClient.connect(); }
}
Add Listeners
#Devoxx #smartvoxx @sarbogast @eloudsa
public class ScheduleActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {
@Override public void onConnected(Bundle bundle) { Wearable.DataApi.addListener(mApiClient, this);}
}
Add Listeners
#Devoxx #smartvoxx @sarbogast @eloudsa
@Override public void onDataChanged(DataEventBuffer dataEventBuffer) { for (DataEvent event : dataEventBuffer) { // Check if we have received our schedules if (event.getType() == DataEvent.TYPE_CHANGED && event.getDataItem().getUri().getPath().startsWith(SCHEDULES_PATH)) { SchedulesListWrapper schedulesListWrapper = new SchedulesListWrapper(); final List<Schedule> schedulesList = schedulesListWrapper.getSchedulesList(event); runOnUiThread(new Runnable() { @Override public void run() { // hide the progress bar findViewById(R.id.progressBar).setVisibility(View.GONE); listViewAdapter.refresh(schedulesList); } }); return; } } }
1
2
3
4
Add Listeners
#Devoxx #smartvoxx @sarbogast @eloudsa
@OverrideprotectedvoidonCreate(BundlesavedInstanceState){ …//ListviewcomponentlistView=(WearableListView)findViewById(R.id.wearable_list); //AssigntheadapterlistViewAdapter=newListViewAdapter(ScheduleActivity.this,newArrayList<Schedule>());listView.setAdapter(listViewAdapter); …}
ListViewAdapter
#Devoxx #smartvoxx @sarbogast @eloudsa
//InnerclassprovidingtheWearableListview'sadapter publicclassListViewAdapterextendsWearableListView.Adapter{ …//Createnewviewsforlistitems//(invokedbytheWearableListView'slayoutmanager) @OverridepublicWearableListView.ViewHolderonCreateViewHolder(ViewGroupparent, intviewType){ //Inflateourcustomlayoutforlistitems returnnewItemViewHolder(newSettingsItemView(mContext));}…
Animation
#Devoxx #smartvoxx @sarbogast @eloudsa
publicfinalclassSettingsItemViewextendsFrameLayoutimplementsWearableListView.OnCenterProximityListener{privateTextViewdescription;publicSettingsItemView(Contextcontext){ super(context);View.inflate(context,R.layout.schedule_row_activity,this); description=(TextView)findViewById(R.id.description); }@OverridepublicvoidonCenterPosition(booleanb){description.animate().scaleX(1f).scaleY(1f).alpha(1); }@OverridepublicvoidonNonCenterPosition(booleanb){description.animate().scaleX(0.8f).scaleY(0.8f).alpha(0.6f); }}
Animation
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" …><TextViewandroid:id="@+id/title"…/><RelativeLayoutandroid:layout_width="fill_parent" android:layout_height="fill_parent"> <android.support.wearable.view.WearableListView android:id="@+id/wearable_list" …></android.support.wearable.view.WearableListView> <ProgressBarandroid:id="@+id/progressBar" …/></RelativeLayout></LinearLayout>
Layout: schedule_rect_activity1
2
3
4
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><mergexmlns:android="http://schemas.android.com/apk/res/android"> <TextViewandroid:id="@+id/description"…/></merge>
Layout: schedule_row_activity
#Devoxx #smartvoxx @sarbogast @eloudsa
Run the app
• Build and deploy on Phone and Watch
• Forwarding ports (adb forward tcp:…):
• Link Phone to Wear Emulator
• Data are retrieved and displayed from Phone
#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)
#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
import WatchKit
class ScheduleRowController: NSObject { }
ScheduleRowController class
#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 5: Select a Schedule
• Store Schedule’s data on Tag
• Add ClickListener on WearableListView
• Attach the listener
• Retrieve Schedule’s data from Tag
• Display the selected item
#Devoxx #smartvoxx @sarbogast @eloudsa
@OverridepublicvoidonBindViewHolder(WearableListView.ViewHolderholder,intposition){ //retrievethetextviewItemViewHolderitemHolder=(ItemViewHolder)holder; TextViewview=itemHolder.textView;//retrieve,transformanddisplaytheschedule'sday Scheduleschedule=mDataset.get(position); StringscheduleDay=schedule.getTitle().replace(",","\n"); view.setText(scheduleDay);//replacelistitem'smetadataholder.itemView.setTag(schedule);}
Store Schedule’s data1
2
#Devoxx #smartvoxx @sarbogast @eloudsa
public class ScheduleActivity extends Activity implements WearableListView.ClickListener, GoogleApiClient.ConnectionCallbacks, DataApi.DataListener {
@Override public void onClick(WearableListView.ViewHolder viewHolder) { }
}
Add ClickListener
#Devoxx #smartvoxx @sarbogast @eloudsa
@Override protected void onCreate(Bundle savedInstanceState) {
… // Assign the adapter listViewAdapter = new ListViewAdapter(ScheduleActivity.this, new ArrayList<Schedule>()); listView.setAdapter(listViewAdapter); // Set the click listener listView.setClickListener(ScheduleActivity.this);
}
Attach the ClickListener
#Devoxx #smartvoxx @sarbogast @eloudsa
@Override public void onClick(WearableListView.ViewHolder viewHolder) { Schedule schedule = (Schedule) viewHolder.itemView.getTag(); if (schedule == null) { return; } Toast.makeText(ScheduleActivity.this, "You tap on: " + schedule.getDay(), Toast.LENGTH_SHORT).show();}
Retrieve and display schedule
1
2
#Devoxx #smartvoxx @sarbogast @eloudsa
Run the app
• Build and deploy on the Watch
• Forwarding ports (adb forward tcp:…):
• Link Phone to Wear Emulator
• Tap on a schedule
#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
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
Phone
Data Layer
Watch
Data Layer
.create(“/path”)
PutDataMapRequest
.putInt(KEY, data)
DataMap
.asPutDataRequest()
PutDataRequest
.putDataItem()
Wearable.DataApi
Data Item (Shared)
.getInt()
DataMap
.getDataItem()
DataEvent
.fromDataItem(DataItem)
DataMapItem
onDataChanged()
DataApi.DataListener
Data API
#Devoxx #smartvoxx @sarbogast @eloudsa
Wear
Data Item (Cache)
.getInt()
DataMap
.getDataIetms()
DataApi
wear://path_to_data
12
Fetch from local cache
MessageApi
sendMessage()
3
4
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassWearServiceextendsWearableListenerService{
//sendSchedulestothewatchprivatevoidsendSchedules(List<Link>schedules){ finalPutDataMapRequestputDataMapRequest=PutDataMapRequest.create(SCHEDULES_PATH); ArrayList<DataMap>schedulesDataMap=newArrayList<>(); //processeachschedulefor(Linkschedule:schedules){finalDataMapscheduleDataMap=newDataMap(); //processandpushschedule'sdatascheduleDataMap.putString("day",Utils.getLastPartUrl(schedule.getHref())); scheduleDataMap.putString("title",schedule.getTitle()); schedulesDataMap.add(scheduleDataMap); }//storethelistinthedatamaptosendittothewatch putDataMapRequest.getDataMap().putDataMapArrayList("/list",schedulesDataMap); //sendthelistif(mApiClient.isConnected()){Wearable.DataApi.putDataItem(mApiClient,putDataMapRequest.asPutDataRequest()); }}
Changes on Phone: No timestamp
1
#Devoxx #smartvoxx @sarbogast @eloudsa
@OverrideprotectedvoidonResume(){super.onResume();//Retrieveanddisplaythelistofschedules getSchedules(SCHEDULES_PATH);}
Display data items
#Devoxx #smartvoxx @sarbogast @eloudsa
private void getSchedules(final String pathToContent) { Uri uri = new Uri.Builder() .scheme(PutDataRequest.WEAR_URI_SCHEME) .path(pathToContent) .build(); Wearable.DataApi.getDataItems(mApiClient, uri) .setResultCallback( new ResultCallback<DataItemBuffer>() { @Override public void onResult(DataItemBuffer dataItems) { if (dataItems.getCount() == 0) { // refresh the list of schedules from Mobile sendMessage(SCHEDULES_PATH, "get list of schedules"); return; } … // retrieve and display the schedule from the cache SchedulesListWrapper schedulesListWrapper = new SchedulesListWrapper(); final List<Schedule> schedulesList = schedulesListWrapper.getSchedulesList(dataMap); runOnUiThread(new Runnable() { @Override public void run() { // hide the progress bar findViewById(R.id.progressBar).setVisibility(View.GONE); listViewAdapter.refresh(schedulesList); } }); } } ); }
Retrieve data: mobile or cache?1
2
3
4
#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
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
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 7: Get Talk
• Create the Activity
• Layouts: GridView, Card, Custom
• GridViewPagerAdapter
• Create the Fragments
• EventBus
• Retrieve Talks and Speakers from Data Api
• Requests sent through the Message Api
• Bonus: Follow on Twitter (Confirmation Activity)
#Devoxx #smartvoxx @sarbogast @eloudsa
GridViewPager
Row 1: • Column 1: Talk information • Column 2: Summary
Row 2: • Column 1: Speaker 1 • Column 2: Speaker 2 • … • Column n: Speaker n
#Devoxx #smartvoxx @sarbogast @eloudsa
Fragment Fragment
Fragment Fragment
• TalkFragment • TalkSummaryFragment • TalkSpeakerFragment
FragmentGridPagerAdapter
TalkActivity
#Devoxx #smartvoxx @sarbogast @eloudsa
• Define the layout
• Create the fragments
• Create the Adapter
• Link the Adapter
Create the GridViewPager
#Devoxx #smartvoxx @sarbogast @eloudsa
<FrameLayoutxmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent"android:layout_height="match_parent"android:background="@color/black"><android.support.wearable.view.GridViewPagerxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/pager"android:layout_width="match_parent" android:layout_height="match_parent" android:keepScreenOn="false"/><android.support.wearable.view.DotsPageIndicator android:id="@+id/page_indicator"android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal|top" app:dotFadeOutDelay="10000"></android.support.wearable.view.DotsPageIndicator> </FrameLayout>
Layoutres/layout/talk_activity.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><android.support.wearable.view.WatchViewStub xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/watch_talk_stub"android:layout_width="match_parent"android:layout_height="match_parent"app:rectLayout="@layout/talk_rect_fragment" app:roundLayout="@layout/talk_round_fragment" tools:context=".TalkActivity"tools:deviceIds="wear"></android.support.wearable.view.WatchViewStub>
Layoutres/layout/talk_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<RelativeLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent"android:layout_height="fill_parent"android:paddingTop="15dp"><TextViewandroid:id="@+id/title"android:layout_width="fill_parent" ….
Layoutres/layout/talk_rect_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<android.support.wearable.view.BoxInsetLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="match_parent"android:padding="15dp"><RelativeLayoutandroid:layout_width="match_parent" android:layout_height="match_parent"> <TextViewandroid:id="@+id/title"
Layoutres/layout/talk_round_fragment.xml
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><android.support.wearable.view.CardScrollViewxmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/card_scroll_view"android:layout_width="match_parent"android:layout_height="match_parent"><android.support.wearable.view.CardFrameandroid:id="@+id/card_frame"…><LinearLayoutandroid:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical"><TextViewandroid:id="@+id/title"
Summary: Layout
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassTalkSummaryFragmentextendsFragment{…description.setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){if(mTalkSummary==null){return;}if(mEllipsize){description.setText(mTalkSummary);}else{description.setText(StringUtils.abbreviate(mTalkSummary,ELLIPSIS_SIZE)); }
mEllipsize=!mEllipsize;//ForcetheCardScrollViewtoresettoitsinitialposition mainView.findViewById(R.id.card_scroll_view).setScrollX(0); mainView.findViewById(R.id.card_scroll_view).setScrollY(0); }});
Summary: Expand content
#Devoxx #smartvoxx @sarbogast @eloudsa
dependencies{compilefileTree(dir:'libs',include:['*.jar']) compile'com.google.android.support:wearable:1.3.0' compile'com.google.android.gms:play-services-wearable:8.1.0' //EventBus:BusWearcompile'pl.tajchert:buswear:0.9.5'…}
EventBusbuild.grade
#Devoxx #smartvoxx @sarbogast @eloudsa
EventBus.getDefault().postLocal(newTalkEvent(mTalk));
EventBus - Sending events
#Devoxx #smartvoxx @sarbogast @eloudsa
publicvoidonEvent(AddFavoriteEventaddFavoritesEvent){
…
}
EventBus - Receive eventsRequired to register/unregister to the bus
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassTalkActivityextendsActivity…{
@OverrideprotectedvoidonStart(){…EventBus.getDefault().register(this);}@OverrideprotectedvoidonStop(){EventBus.getDefault().unregister(this);…}
EventBus - (Un)Register
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear">
…<activityandroid:name="android.support.wearable.activity.ConfirmationActivity"/>
…
Confirmation animationUpdate the manifest of the watch
#Devoxx #smartvoxx @sarbogast @eloudsa
@OverridepublicViewonCreateView(LayoutInflaterinflater,ViewGroupcontainer, BundlesavedInstanceState){
mainView.findViewById(R.id.twitterIcon).setOnClickListener(newView.OnClickListener(){@OverridepublicvoidonClick(Viewv){startConfirmationActivity(ConfirmationActivity.OPEN_ON_PHONE_ANIMATION,getString(R.string.confirmation_open_on_phone));EventBus.getDefault().postLocal(newConfirmationEvent(TWITTER_PATH,(String)mainView.findViewById(R.id.twitterIcon).getTag())); }});
Confirmation animation (Listener)
1
2
#Devoxx #smartvoxx @sarbogast @eloudsa
privatevoidstartConfirmationActivity(intanimationType,Stringmessage){IntentconfirmationActivity=newIntent(getActivity(),ConfirmationActivity.class) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_ANIMATION) .putExtra(ConfirmationActivity.EXTRA_ANIMATION_TYPE,animationType).putExtra(ConfirmationActivity.EXTRA_MESSAGE,message); getActivity().startActivity(confirmationActivity);
}
Animation type
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassWearServiceextendsWearableListenerService{…privatevoidfollowOnTwitter(StringinputData){ StringtwitterName=inputData==null?"":inputData.trim().toLowerCase(); twitterName=twitterName.replaceFirst("@",""); if(twitterName.isEmpty()){return;}Intentintent=newIntent(Intent.ACTION_VIEW,Uri.parse("https://twitter.com/"+twitterName)); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); this.startActivity(intent);}
}
Open Twitter on Phone
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 7: Square GridViewPagerCardCustom
Custom CustomWearableListView
Step 7: Done!
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 7: RoundCardCustom
Custom CustomWearableListView
GridViewPager
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Set Favorites• Retrieve, add, remove favorites on the calendar
• Add CalendarHelper on the phone
• Favorite messages sent over DataApi (retrieved, added, removed)
• Add reminders using AlarmManager
• Manage a manual delete from the Calendar with a synchronisation of the SlotsActivity or TalkActivity
• Send a message when required (retrieve) or when the favorite has changed (add or remove)
• Use the EventBus to synchronise the components (Activity, Fragments)
• ConfirmationActivity on favorite’s action
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: FavoritesPhone Watch
12
3
4eventId
removeread
add
Calendar
removeread
add
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: RemindersPhone
1
Calendaradd
AlarmManager
2
10 minutes before
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Alarm ServicePhone
1
Calendaradd
AlarmManager
2
10 minutes before
AlarmService
talkId, title, eventId, schedule, …
3
#Devoxx #smartvoxx @sarbogast @eloudsa
Step 8: Wake-upPhone
AlarmManager
AlarmService
talkId, title, eventId, schedule, …1
AlarmReceiver
talkId, title, eventId, schedule, …
.broadcast()
2
#Devoxx #smartvoxx @sarbogast @eloudsa
<?xmlversion="1.0"encoding="utf-8"?><manifestxmlns:android="http://schemas.android.com/apk/res/android" package="net.noratek.smartvoxxwear"><!--Add,Readandremovefavoritesonthecalendar--> <uses-permissionandroid:name="android.permission.READ_CALENDAR"/> <uses-permissionandroid:name="android.permission.WRITE_CALENDAR"/>
Calendar: Add permissionsUpdate the manifest of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
<application>…
<serviceandroid:name=".alarm.AlarmService"/><receiverandroid:name=".alarm.AlarmReceiver"><intent-filter><actionandroid:name="net.noratek.smartvoxxwear.AlarmService.BROADCAST"/> </intent-filter></receiver>
</application
Alarm: Service, ReceiverUpdate the manifest of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
Mobile: event on Calendar
• Title
• Room
• Summary
• Schedule
#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
Phone: Prepare and SendPhone
AlarmManagerAlarmReceiver
Custom notification
Action EventService
3
4
AlarmService
talkId, title, eventId, schedule, …1
.broadcast()
2
#Devoxx #smartvoxx @sarbogast @eloudsa
//CreateanintentforthereplyactionIntentactionIntent=newIntent(context,EventService.class);actionIntent.putExtras(bundle);PendingIntentactionPendingIntent=PendingIntent.getService(context,0,actionIntent, PendingIntent.FLAG_UPDATE_CURRENT); //CreatetheactionNotificationCompat.Actionaction=newNotificationCompat.Action.Builder(R.drawable.ic_calendar, context.getText(R.string.remove_event),actionPendingIntent).build();
Notification: Action to Service
#Devoxx #smartvoxx @sarbogast @eloudsa
//Addanotificationwiththesameactiononmobileandwatch NotificationCompat.BuildermBuilder=newNotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_logo) .setContentTitle(talk.getTitle()) .setContentText(information).setAutoCancel(true).setVibrate(newlong[]{1000,1000,1000,1000,1000,1000}) .setDefaults(Notification.DEFAULT_ALL) .addAction(action);
NotificationCompat.WearableExtenderwearableExtender=newNotificationCompat.WearableExtender(mBuilder.build()); Bitmapbitmap=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_black); wearableExtender.setBackground(bitmap);wearableExtender.extend(mBuilder);NotificationManagerCompatmanager=NotificationManagerCompat.from(context); manager.notify(notificationId,mBuilder.build());
Notification: Builder
#Devoxx #smartvoxx @sarbogast @eloudsa
//Addanotificationwithanactiononlyvisibleonthewatch NotificationCompat.BuildermBuilder=newNotificationCompat.Builder(context).setSmallIcon(R.drawable.ic_logo) .setContentTitle(talk.getTitle()) .setContentText(information).setAutoCancel(true).setVibrate(newlong[]{1000,1000,1000,1000,1000,1000}) .setDefaults(Notification.DEFAULT_ALL) .extend(newNotificationCompat.WearableExtender().addAction(action));
NotificationCompat.WearableExtenderwearableExtender=newNotificationCompat.WearableExtender(mBuilder.build()); Bitmapbitmap=BitmapFactory.decodeResource(context.getResources(),R.drawable.ic_black); wearableExtender.setBackground(bitmap);wearableExtender.extend(mBuilder);NotificationManagerCompatmanager=NotificationManagerCompat.from(context); manager.notify(notificationId,mBuilder.build());
Wearable-only action
#Devoxx #smartvoxx @sarbogast @eloudsa
Phone: EventServicePhone
Calendar
EventServiceremove Event
2
1
3
Event removed
#Devoxx #smartvoxx @sarbogast @eloudsa
<application>…
<serviceandroid:name=".service.EventService"/>
</application
Alarm: EventServiceUpdate the manifest of the phone
#Devoxx #smartvoxx @sarbogast @eloudsa
publicclassEventServiceextendsService{
@OverridepublicintonStartCommand(Intentintent,intflags,intstartId){…//removetheeventfromthecalendarCalendarHelpercalendarHelper=newCalendarHelper(this); calendarHelper.removeEvent(eventId);
…sendFavorite(talkId,0L);
..}
Phone: EventService
#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
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 10:Glances and Complications
• Glance controller
• Complication 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
Prepare the build
• Include all permissions of wearable into Phone
• Same package name and version number
#Devoxx #smartvoxx @sarbogast @eloudsa
Mobile APK embeds WearablePhone App Module
Code
Resources
Wearable App
Watch App Module
Code
Resources
#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: Release the App
• Package the Apple Watch app with the iPhone app
• Release the iPhone app like any other
• Wait for review…
• Wait again…
• Wait some more…
#Devoxx #smartvoxx @sarbogast @eloudsa
A word about Pebble
• Language: either C or Javascript
• Development environment: either text editor or CloudPebble
• Platform support: both iOS and Android (+SDKs)
• Devices: Pebble Classic, Pebble Time, Pebble Time Round
• Distribution: via the Pebble app
#Devoxx #smartvoxx @sarbogast @eloudsa
staticboolload_shutter_group_list(){if(accessToken&&sizeof(accessToken)>0){DictionaryIterator*iter;app_message_outbox_begin(&iter);if(iter==NULL){APP_LOG(APP_LOG_LEVEL_DEBUG,"nulliter");returnfalse;}
Tupletmessage_type_tuple=TupletInteger(MESSAGE_TYPE,LOAD_SHUTTER_GROUP_LIST);dict_write_tuplet(iter,&message_type_tuple);Tupletaccess_token_tuple=TupletCString(ACCESS_TOKEN,accessToken);dict_write_tuplet(iter,&access_token_tuple);Tupletrefresh_token_tuple=TupletCString(REFRESH_TOKEN,refreshToken);dict_write_tuplet(iter,&refresh_token_tuple);Tupletsite_id_tuple=TupletInteger(SITE_ID,selected_site_id);dict_write_tuplet(iter,&site_id_tuple);dict_write_end(iter);
app_message_outbox_send();
returntrue;}else{returnfalse;}}
Pebble code
#Devoxx #smartvoxx @sarbogast @eloudsa
functionloadShutterGroupList(accessToken,refreshToken,args){ console.log("Loadingshuttergrouplistforaccesstoken"+accessToken+"andsite"+args[0]); varresponse; varreq=newXMLHttpRequest(); //buildtheGETrequest varurl="https://api.myfox.me:443/v2/site/"+args[0]+"/group/shutter/items?access_token="+accessToken; console.log("GETting"+url); req.open('GET',url,true); req.onload=function(e){ if(req.readyState==4){ //200-HTTPOK if(req.status==200){ console.log(req.responseText); response=JSON.parse(req.responseText); varshutterGroupList; if(response.status==='OK'){ shutterGroupList=response.payload.items;
varmsg={}; msg.messageType=MessageType.SHUTTER_GROUP_LIST; for(vari=0;i<shutterGroupList.length;i++){ varshutterGroup=shutterGroupList[i]; msg[''+shutterGroup.groupId]=shutterGroup.label; } console.log("SendingresponsebacktoPebble:"+JSON.stringify(msg)); Pebble.sendAppMessage(msg); }else{ console.log("StatusnotOK"); Pebble.sendAppMessage({messageType:MessageType.ERROR,errorMessage:"Couldnotloadshuttergroups."}); } }elseif(req.status==401&&refreshToken){ getNewAccessToken(refreshToken,loadShutterGroupList,args); }else{ console.log("Requestreturnederrorcode"+req.status.toString()); Pebble.sendAppMessage({messageType:MessageType.ERROR,errorMessage:"Couldnotloadshuttergroups."}); } } }; req.send(null);}
Pebble code
#Devoxx #smartvoxx @sarbogast @eloudsa
Summary
• Huge inequalities in terms of development platform ease-of-use
• Apple obviously took time to add abstraction layers that make development more expressive
• Short learning curve on Android Wear compared to Apple Watch
• Tooling support not up-to-date on Android
• Documentation is not really finished for both platforms
• Not all apps make sense on smartwatches
#Devoxx #smartvoxx @sarbogast @eloudsa
Apps that work on smartwatches• countdowns and timers
• status checks: what’s the temperature? what’s my next session? what’s the score of the game?
• remote controls: switch off the light, change the music, open my hotel room, pay for my shopping
• notification responders: invitation to a meeting -> what’s the meeting about, somebody sent me a message -> what does it say?
• data trackers: where am I? how many calories am I burning? what’s my speed?)
#Devoxx #smartvoxx @sarbogast @eloudsa
Apps that don’t make sense
• games of any kind
• any long reading (news, books, etc.)
• ecommerce
• video or image viewing
• anything that requires text input