Google Fit, Android Wear & Xamarin
-
Upload
peter-friese -
Category
Mobile
-
view
300 -
download
3
Transcript of Google Fit, Android Wear & Xamarin
Google Fit, Wear & XamarinPeter Friese @peterfriese Matt Sullivan @mjohnsullivan
Code Lab
Food for Thought
Are Fitness apps just for runners, cyclists, athletes?
Or for those on a mission to get fit and stay healthy?
Can we add elements of fitness to everyday apps?
http://handy-games.com/games/kitty-my-fitness-cat/
Positive impact on physical and emotional wellbeing
== positive impact on user
experience and happiness
Android Wear
Create a Wear app module
Design the watch face UI
Extend WatchFaceService.Engine
Create a companion app
1
2
3
4
Watch Face Architecture
Wearable App
CanvasWatchFaceService
Engine
WearableConfigActivity
WearableListenerService
Mobile App
MobileConfigActivity
Design for square and round
Interactive and Ambient Modes
Gestures
CanvasWatchFaceService.Engine
UI DrawingManually draw onto a Canvas Object
Ambient Mode ChangesReduced color spaceBurn protection
Redraw Intervals
Once a minute in Ambient – OnTimeTick()
Custom for interactive – use a System Timer
TimeZone ChangesUse a BroadcastReceiver
Interactive Watch Faces.SetAcceptsTapEvents() in BuilderOverride OnTapCommand()
Drawing the watch face
Background
Hour TicksStep Count
Minutes Hand
Seconds Hand
Hours Hand
backgroundScaledBitmap = Bitmap.CreateScaledBitmap(backgroundBitmap, width, height, true); canvas.DrawBitmap(backgroundScaledBitmap, 0, 0, null);
Drawing the Watch Face - Background
var innerTickRadius = centerX - 10; var outerTickRadius = centerX; for (var tickIndex = 0; tickIndex < 12; tickIndex++) { var tickRot = (float)(tickIndex * Math.PI * 2 / 12); var innerX = (float)Math.Sin(tickRot) * innerTickRadius; var innerY = (float)-Math.Cos(tickRot) * innerTickRadius; var outerX = (float)Math.Sin(tickRot) * outerTickRadius; var outerY = (float)-Math.Cos(tickRot) * outerTickRadius; canvas.DrawLine(centerX + innerX, centerY + innerY, centerX + outerX, centerY + outerY, tickPaint);}
Drawing the Watch Face - Ticks
var width = bounds.Width(); var height = bounds.Height(); var steps = stepCount.ToString(); var stepsWidth = stepcountPaint.MeasureText(steps);var x = width - stepsWidth - stepCountOffset; var y = (height + stepCountTextSize) / 2.0f; canvas.DrawText(steps, x, y, stepcountPaint);
Drawing the Watch Face - Step Count
Ambient mode
Reduce color depth Reduce number of non-black pixels Use color for < 5% pixelsWatch face updates once a minute - don’t show seconds
Save power
Interactive Ambient
public override void OnDraw(Canvas canvas, Rect bounds){ DrawFace(canvas, bounds); DrawHands(canvas, bounds); } void DrawHands(Canvas canvas, Rect bounds){ if (!IsInAmbientMode) { canvas.DrawLine(centerX, centerY, centerX + secX, centerY + secY, secondPaint); } canvas.DrawLine(centerX, centerY, centerX + minX, centerY + minY, minutePaint);}
Ambient Mode
public override void OnDraw(Canvas canvas, Rect bounds){ DrawFace(canvas, bounds); DrawHands(canvas, bounds); } void DrawHands(Canvas canvas, Rect bounds){ if (!IsInAmbientMode) { canvas.DrawLine(centerX, centerY, centerX + secX, centerY + secY, secondPaint); } canvas.DrawLine(centerX, centerY, centerX + minX, centerY + minY, minutePaint);}
Ambient Mode
No seconds hand in ambient mode
void UpdateTimer() { if (timerSeconds == null) { timerSeconds = new System.Threading.Timer( state => { Invalidate(); }, null, dueTime, period } else { if (IsVisible && !IsInAmbientMode) { timerSeconds.Change(0, InteractiveUpdateRateMs); } else { timerSeconds.Change(System.Threading.Timeout.Infinite, 0); } } }
Ambient Mode / 2
void UpdateTimer() { if (timerSeconds == null) { timerSeconds = new System.Threading.Timer( state => { Invalidate(); }, null, dueTime, period } else { if (IsVisible && !IsInAmbientMode) { timerSeconds.Change(0, InteractiveUpdateRateMs); } else { timerSeconds.Change(System.Threading.Timeout.Infinite, 0); } } }
Ambient Mode / 2
Redraw every second when in interactive mode
void UpdateTimer() { if (timerSeconds == null) { timerSeconds = new System.Threading.Timer( state => { Invalidate(); }, null, dueTime, period } else { if (IsVisible && !IsInAmbientMode) { timerSeconds.Change(0, InteractiveUpdateRateMs); } else { timerSeconds.Change(System.Threading.Timeout.Infinite, 0); } } }
Ambient Mode / 2
Turn timer on in interactive mode off in ambient mode
Configuration Activities
WEARABLE_CONFIGURATION on Wear
COMPANION_CONFIGURATION on Phone
DataLayer APIListen for Data Item EventsWrite DataItems to URI Paths
void SaveStepCountStatus(GoogleApiClient googleApiClient, bool stepCountOn) { var putDataMapRequest = PutDataMapRequest.Create("/xfit_watchface"); putDataMapRequest.DataMap.PutBoolean("stepcount", stepCountOn); var putDataRequest = putDataMapRequest.AsPutDataRequest(); putDataRequest.SetUrgent(); WearableClass.DataApi.PutDataItem(googleApiClient, putDataRequest); }
Syncing configuration /1
void SaveStepCountStatus(GoogleApiClient googleApiClient, bool stepCountOn) { var putDataMapRequest = PutDataMapRequest.Create("/xfit_watchface"); putDataMapRequest.DataMap.PutBoolean("stepcount", stepCountOn); var putDataRequest = putDataMapRequest.AsPutDataRequest(); putDataRequest.SetUrgent(); WearableClass.DataApi.PutDataItem(googleApiClient, putDataRequest); }
Syncing configuration /1
URI PathData Item KeySync now
void OnDataChanged(DataEventBuffer dataEvents) { foreach (var dataEvent in dataEvents) { var dataItem = dataEvent.DataItem; if (dataItem.Uri.Path == "/xfit_watchface") { var dataMap = DataMapItem.FromDataItem(dataItem).DataMap; displayStepCount = dataMap.GetBoolean(“stepcount”); Invalidate(); // force redraw } } }
Syncing configuration /2
void OnDataChanged(DataEventBuffer dataEvents) { foreach (var dataEvent in dataEvents) { var dataItem = dataEvent.DataItem; if (dataItem.Uri.Path == "/xfit_watchface") { var dataMap = DataMapItem.FromDataItem(dataItem).DataMap; displayStepCount = dataMap.GetBoolean(“stepcount”); Invalidate(); // force redraw } } }
Implements IDataApiDataListener
Syncing configuration /2
URI Path
Data Item Key
Watch face drawing code
GoogleAPIClient code
Configuration code
Reading the step count code
1
2
3
4
Clean Code
Partial Classes
XFitWatchfaceService
XFitWatchFaceEngine (Drawing Code)
XFitWatchFaceEngine (Config Code)
XFitWatchFaceEngine (Google API Client Code)
XFitWatchFaceEngine (Step Count Code)
XFitWatchFaceEngine
Partial Classes
XFitWatchfaceService
XFitWatchFaceEngine (Drawing Code)
XFitWatchFaceEngine (Config Code)
XFitWatchFaceEngine (Google API Client Code)
XFitWatchFaceEngine (Step Count Code)
XFitWatchFaceEngine
[Service(Label = "XFit Watchface 4", Permission = "android.permission.BIND_WALLPAPER")] [IntentFilter(new[] { "android.service.wallpaper.WallpaperService" }, Categories = new[] { "com.google.android.wearable.watchface.category.WATCH_FACE" })] public partial class XFitWatchfaceService: CanvasWatchFaceService { public override WallpaperService.Engine OnCreateEngine() { return new XFitWatchfaceEngine(this); } partial class XFitWatchfaceEngine : Engine { // drawing code } }
XFitWatchfaceService.cs (Drawing Code)
Partial Classes
XFitWatchfaceService
XFitWatchFaceEngine (Drawing Code)
XFitWatchFaceEngine (Config Code)
XFitWatchFaceEngine (Google API Client Code)
XFitWatchFaceEngine (Step Count Code)
XFitWatchFaceEngine
[MetaData( "com.google.android.wearable.watchface.wearableConfigurationAction", Value = "XFitWatchface.CONFIG")] [MetaData( "com.google.android.wearable.watchface.companionConfigurationAction", Value = "XFitWatchface.CONFIG")] public partial class XFitWatchfaceService : CanvasWatchFaceService { partial class XFitWatchfaceEngine : Engine, IDataApiDataListener { void ConnectWatchfaceConfigUpdates() { } } }
XFitWatchfaceServiceConfig.cs (Config Code)
Partial Classes
XFitWatchfaceService
XFitWatchFaceEngine (Drawing Code)
XFitWatchFaceEngine (Config Code)
XFitWatchFaceEngine (Google API Client Code)
XFitWatchFaceEngine (Step Count Code)
XFitWatchFaceEngine
public partial class XFitWatchfaceService : CanvasWatchFaceService { partial class XFitWatchfaceEngine : Engine { GoogleApiClient googleApiClient; void ConnectGoogleApiClient() { googleApiClient = new GoogleApiClient.Builder(owner) .AddApi(FitnessClass.HISTORY_API) .AddApi(FitnessClass.RECORDING_API) .AddApi(WearableClass.API) .Build(); googleApiClient.Connect(); } } }
XFitWatchfaceServiceGoogleApiClient.cs (Google API Client)
Google Fit
Activity Read/Write
Body Read/Write
Location Read/Write
Nutrition Read/Write
Sleep Duration & Type
Calories Consumed, Macronutrients
Height, Weight, BMI, Body Fat % Heart Rate
Distance Traveled Steps Calories Burned Activity Segments (walking, running, biking) Gym Activities
ACTIVITY
NUTRITION
BODY
SLEEP
SENSORS API
RECORDING API
HISTORY API
SESSIONS API
SENSORS API
Find all sensors, both on and off-device FindDataSources (GoogleApiClient c, DataSourcesRequest r)
Register for sensor data Add (GoogleApiClient c, SensorRequest r, IOnDataPointListener l);
Unregister for sensor data Remove (GoogleApiClient c, IOnDataPointListener l);
RECORDING API
Stop recording data types/sources Unsubscribe (GoogleApiClient c, DataType t) Unsubscribe (GoogleApiClient c, DataSource s)
What data is being recorded ListSubscriptions (GoogleApiClient c)
Record from data types/sources Subscribe (GoogleApiClient c, DataType t) Subscribe (GoogleApiClient c, DataSource s)
HISTORY API
Manually add data InsertData (GoogleApiClient c, DataSet d)
Remove data DeleteData (GoogleApiClient c, DeleteDataRequest r)
Retrieve data void ReadData (GoogleApiClient c, DataReadRequest r)
SESSIONS API
Start recording a live session void StartSession(GoogleApiClient client, Session session);
Stop recording a live session void StopSession (GoogleApiClient client, String identifier);
Insert a session void InsertSession (GoogleApiClient client, SessionInsertRequest request)
curl \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ https://www.googleapis.com/fitness/v1/users/me/dataSources/\ derived:com.google.step_count.delta:...:estimated_steps/\ datasets/1438701040000000000-143930584000000000
REST API
Reading Steps
Create a GoogleApiClient
Ensure device is recording steps
Build an aggregated query
Read and extract the data
1
2
3
4
void BuildApiClient () { googleApiClient = new GoogleApiClient.Builder (this) .AddApi (FitnessClass.HISTORY_API) .AddApi (FitnessClass.RECORDING_API) .AddScope (FitnessClass.ScopeActivityReadWrite) .EnableAutoManage (this, AuthClientId, result => { // Unresolvable error, handle or fail silently }) .Build (); SubscribeToSteps (); ReadSteps (); }
Read before connection established
Auto manage connection
Build API Client
void SubscribeToSteps () { FitnessClass.RecordingApi.Subscribe (googleApiClient, TypeStepCountDelta) .SetResultCallback( (IResult result) => { if (!result.Status.IsSuccess) { // Error subscribing, handle } }); }
Subscribing
// Read step count deltas from the History API DataReadRequest DailyStepsForWeek() { long midnight = TimeUtility.MidnightLocalPosix (); long midnightWeekAgo = TimeUtility.DaysAgoPosix (midnight, 7);
return new DataReadRequest.Builder () .Aggregate (DataType.TypeStepCountDelta, DataType.AggregateStepCountDelta) .BucketByTime (1, TimeUnit.Days) .SetTimeRange (midnightWeekAgo, midnight, TimeUnit.Milliseconds) .Build(); }
Data Aggregation
Data Read Request
// Reads and extracts step data void ReadDailySteps () { DataReadRequest readRequest = RequestBuilder.DailyStepsForWeek (); FitnessClass.HistoryApi.ReadData (googleApiClient, readRequest) .SetResultCallback ( (IResult result) => { IList<Bucket> buckets = ((DataReadResult)result).Buckets; foreach (var bucket in buckets) { var stepCount = bucket.DataSets.Sum(dataSet => dataSet.DataPoints.Sum (dp => dp.DataType.Fields.Sum (f => dp.GetValue (f).AsInt()))); Log.Info (Tag, $"Step Count: {stepCount}"); } }); }
Reading Data
Extracting Data
Read Daily Steps
Writing Data
// Writing data googleApiClient = new GoogleApiClient.Builder(context) .AddApi(...) .AddConnectionCallbacks( connectionHint => { // Connected! }, cause => { // Suspended } ).Build();
Writes require an established connection
Writing Data
Reading Today’s Steps
Create a GoogleApiClient
Ensure device is recording steps
Build an aggregated query
Read and extract the data
1
2
3
3
Create a GoogleApiClient
Ensure device is recording steps
Read and extract the data
1
2
2
// All in one reading steps void AllInOne() { var client = new GoogleApiClient.Builder (this) .AddApi (FitnessClass.HISTORY_API) .UseDefaultAccount () .Build (); client.Connect ();
FitnessClass.HistoryApi.ReadDailyTotal (client, TypeStepCountDelta) .SetResultCallback ( (IResult result) => { DataSet ds = ((DailyTotalResult) result).Total; int stepCount = ds.DataPoints.Sum ( dp => dp.DataType.Fields.Sum ( f => dp.GetValue (f).AsInt())); }); }
Default Account Read Daily Total
Read Today’s Steps
Code Lab
Thank You!https://developers.google.com/fit/https://developer.android.com/wear/http://developer.android.com/training/wearables/watch-faces/