Google Fit, Android Wear & Xamarin

Post on 16-Apr-2017

300 views 3 download

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/