Skip to content

Most visited

Recently visited

navigation
WatchFace / Wearable / src / com.example.android.wearable.watchface /

FitStepsWatchFaceService.java

1
/*
2
 * Copyright (C) 2014 The Android Open Source Project
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *      http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
 
17
package com.example.android.wearable.watchface;
18
 
19
import com.google.android.gms.common.ConnectionResult;
20
import com.google.android.gms.common.api.GoogleApiClient;
21
import com.google.android.gms.common.api.PendingResult;
22
import com.google.android.gms.common.api.ResultCallback;
23
import com.google.android.gms.common.api.Status;
24
import com.google.android.gms.fitness.Fitness;
25
import com.google.android.gms.fitness.FitnessStatusCodes;
26
import com.google.android.gms.fitness.data.DataPoint;
27
import com.google.android.gms.fitness.data.DataType;
28
import com.google.android.gms.fitness.data.Field;
29
import com.google.android.gms.fitness.result.DailyTotalResult;
30
 
31
import android.content.BroadcastReceiver;
32
import android.content.Context;
33
import android.content.Intent;
34
import android.content.IntentFilter;
35
import android.content.res.Resources;
36
import android.graphics.Canvas;
37
import android.graphics.Color;
38
import android.graphics.Paint;
39
import android.graphics.Rect;
40
import android.graphics.Typeface;
41
import android.os.Bundle;
42
import android.os.Handler;
43
import android.os.Message;
44
import android.support.wearable.watchface.CanvasWatchFaceService;
45
import android.support.wearable.watchface.WatchFaceStyle;
46
import android.text.format.DateFormat;
47
import android.util.Log;
48
import android.view.SurfaceHolder;
49
import android.view.WindowInsets;
50
 
51
import java.util.Calendar;
52
import java.util.List;
53
import java.util.TimeZone;
54
import java.util.concurrent.TimeUnit;
55
 
56
/**
57
 * The step count watch face shows user's daily step total via Google Fit (matches Google Fit app).
58
 * Steps are polled initially when the Google API Client successfully connects and once a minute
59
 * after that via the onTimeTick callback. If you want more frequent updates, you will want to add
60
 * your own  Handler.
61
 *
62
 * Authentication is not a requirement to request steps from Google Fit on Wear.
63
 *
64
 * In ambient mode, the seconds are replaced with an AM/PM indicator.
65
 *
66
 * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
67
 * require burn-in protection, the hours are drawn in normal rather than bold.
68
 *
69
 */
70
public class FitStepsWatchFaceService extends CanvasWatchFaceService {
71
 
72
    private static final String TAG = "StepCountWatchFace";
73
 
74
    private static final Typeface BOLD_TYPEFACE =
75
            Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
76
    private static final Typeface NORMAL_TYPEFACE =
77
            Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
78
 
79
    /**
80
     * Update rate in milliseconds for active mode (non-ambient).
81
     */
82
    private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
83
 
84
    @Override
85
    public Engine onCreateEngine() {
86
        return new Engine();
87
    }
88
 
89
    private class Engine extends CanvasWatchFaceService.Engine implements
90
            GoogleApiClient.ConnectionCallbacks,
91
            GoogleApiClient.OnConnectionFailedListener,
92
            ResultCallback<DailyTotalResult> {
93
 
94
        private static final int BACKGROUND_COLOR = Color.BLACK;
95
        private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
96
        private static final int TEXT_SECONDS_COLOR = Color.GRAY;
97
        private static final int TEXT_AM_PM_COLOR = Color.GRAY;
98
        private static final int TEXT_COLON_COLOR = Color.GRAY;
99
        private static final int TEXT_STEP_COUNT_COLOR = Color.GRAY;
100
 
101
        private static final String COLON_STRING = ":";
102
 
103
        private static final int MSG_UPDATE_TIME = 0;
104
 
105
        /* Handler to update the time periodically in interactive mode. */
106
        private final Handler mUpdateTimeHandler = new Handler() {
107
            @Override
108
            public void handleMessage(Message message) {
109
                switch (message.what) {
110
                    case MSG_UPDATE_TIME:
111
                        if (Log.isLoggable(TAG, Log.VERBOSE)) {
112
                            Log.v(TAG, "updating time");
113
                        }
114
                        invalidate();
115
                        if (shouldUpdateTimeHandlerBeRunning()) {
116
                            long timeMs = System.currentTimeMillis();
117
                            long delayMs =
118
                                    ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
119
                            mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
120
                        }
121
                        break;
122
                }
123
            }
124
        };
125
 
126
        /**
127
         * Handles time zone and locale changes.
128
         */
129
        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
130
            @Override
131
            public void onReceive(Context context, Intent intent) {
132
                mCalendar.setTimeZone(TimeZone.getDefault());
133
                invalidate();
134
            }
135
        };
136
 
137
        /**
138
         * Unregistering an unregistered receiver throws an exception. Keep track of the
139
         * registration state to prevent that.
140
         */
141
        private boolean mRegisteredReceiver = false;
142
 
143
        private Paint mHourPaint;
144
        private Paint mMinutePaint;
145
        private Paint mSecondPaint;
146
        private Paint mAmPmPaint;
147
        private Paint mColonPaint;
148
        private Paint mStepCountPaint;
149
 
150
        private float mColonWidth;
151
 
152
        private Calendar mCalendar;
153
 
154
        private float mXOffset;
155
        private float mXStepsOffset;
156
        private float mYOffset;
157
        private float mLineHeight;
158
 
159
        private String mAmString;
160
        private String mPmString;
161
 
162
 
163
        /**
164
         * Whether the display supports fewer bits for each color in ambient mode. When true, we
165
         * disable anti-aliasing in ambient mode.
166
         */
167
        private boolean mLowBitAmbient;
168
 
169
        /*
170
         * Google API Client used to make Google Fit requests for step data.
171
         */
172
        private GoogleApiClient mGoogleApiClient;
173
 
174
        private boolean mStepsRequested;
175
 
176
        private int mStepsTotal = 0;
177
 
178
        @Override
179
        public void onCreate(SurfaceHolder holder) {
180
            if (Log.isLoggable(TAG, Log.DEBUG)) {
181
                Log.d(TAG, "onCreate");
182
            }
183
 
184
            super.onCreate(holder);
185
 
186
            mStepsRequested = false;
187
            mGoogleApiClient = new GoogleApiClient.Builder(FitStepsWatchFaceService.this)
188
                    .addConnectionCallbacks(this)
189
                    .addOnConnectionFailedListener(this)
190
                    .addApi(Fitness.HISTORY_API)
191
                    .addApi(Fitness.RECORDING_API)
192
                    // When user has multiple accounts, useDefaultAccount() allows Google Fit to
193
                    // associated with the main account for steps. It also replaces the need for
194
                    // a scope request.
195
                    .useDefaultAccount()
196
                    .build();
197
 
198
            setWatchFaceStyle(new WatchFaceStyle.Builder(FitStepsWatchFaceService.this)
199
                    .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
200
                    .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
201
                    .setShowSystemUiTime(false)
202
                    .build());
203
 
204
            Resources resources = getResources();
205
 
206
            mYOffset = resources.getDimension(R.dimen.fit_y_offset);
207
            mLineHeight = resources.getDimension(R.dimen.fit_line_height);
208
            mAmString = resources.getString(R.string.fit_am);
209
            mPmString = resources.getString(R.string.fit_pm);
210
 
211
            mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
212
            mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
213
            mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
214
            mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
215
            mColonPaint = createTextPaint(TEXT_COLON_COLOR);
216
            mStepCountPaint = createTextPaint(TEXT_STEP_COUNT_COLOR);
217
 
218
            mCalendar = Calendar.getInstance();
219
 
220
        }
221
 
222
        @Override
223
        public void onDestroy() {
224
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
225
            super.onDestroy();
226
        }
227
 
228
        private Paint createTextPaint(int color) {
229
            return createTextPaint(color, NORMAL_TYPEFACE);
230
        }
231
 
232
        private Paint createTextPaint(int color, Typeface typeface) {
233
            Paint paint = new Paint();
234
            paint.setColor(color);
235
            paint.setTypeface(typeface);
236
            paint.setAntiAlias(true);
237
            return paint;
238
        }
239
 
240
        @Override
241
        public void onVisibilityChanged(boolean visible) {
242
            if (Log.isLoggable(TAG, Log.DEBUG)) {
243
                Log.d(TAG, "onVisibilityChanged: " + visible);
244
            }
245
            super.onVisibilityChanged(visible);
246
 
247
            if (visible) {
248
                mGoogleApiClient.connect();
249
 
250
                registerReceiver();
251
 
252
                // Update time zone and date formats, in case they changed while we weren't visible.
253
                mCalendar.setTimeZone(TimeZone.getDefault());
254
            } else {
255
                unregisterReceiver();
256
 
257
                if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
258
                    mGoogleApiClient.disconnect();
259
                }
260
            }
261
 
262
            // Whether the timer should be running depends on whether we're visible (as well as
263
            // whether we're in ambient mode), so we may need to start or stop the timer.
264
            updateTimer();
265
        }
266
 
267
 
268
        private void registerReceiver() {
269
            if (mRegisteredReceiver) {
270
                return;
271
            }
272
            mRegisteredReceiver = true;
273
            IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
274
            FitStepsWatchFaceService.this.registerReceiver(mReceiver, filter);
275
        }
276
 
277
        private void unregisterReceiver() {
278
            if (!mRegisteredReceiver) {
279
                return;
280
            }
281
            mRegisteredReceiver = false;
282
            FitStepsWatchFaceService.this.unregisterReceiver(mReceiver);
283
        }
284
 
285
        @Override
286
        public void onApplyWindowInsets(WindowInsets insets) {
287
            if (Log.isLoggable(TAG, Log.DEBUG)) {
288
                Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
289
            }
290
            super.onApplyWindowInsets(insets);
291
 
292
            // Load resources that have alternate values for round watches.
293
            Resources resources = FitStepsWatchFaceService.this.getResources();
294
            boolean isRound = insets.isRound();
295
            mXOffset = resources.getDimension(isRound
296
                    ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
297
            mXStepsOffset =  resources.getDimension(isRound
298
                    ? R.dimen.fit_steps_or_distance_x_offset_round : R.dimen.fit_steps_or_distance_x_offset);
299
            float textSize = resources.getDimension(isRound
300
                    ? R.dimen.fit_text_size_round : R.dimen.fit_text_size);
301
            float amPmSize = resources.getDimension(isRound
302
                    ? R.dimen.fit_am_pm_size_round : R.dimen.fit_am_pm_size);
303
 
304
            mHourPaint.setTextSize(textSize);
305
            mMinutePaint.setTextSize(textSize);
306
            mSecondPaint.setTextSize(textSize);
307
            mAmPmPaint.setTextSize(amPmSize);
308
            mColonPaint.setTextSize(textSize);
309
            mStepCountPaint.setTextSize(resources.getDimension(R.dimen.fit_steps_or_distance_text_size));
310
 
311
            mColonWidth = mColonPaint.measureText(COLON_STRING);
312
        }
313
 
314
        @Override
315
        public void onPropertiesChanged(Bundle properties) {
316
            super.onPropertiesChanged(properties);
317
 
318
            boolean burnInProtection = properties.getBoolean(PROPERTY_BURN_IN_PROTECTION, false);
319
            mHourPaint.setTypeface(burnInProtection ? NORMAL_TYPEFACE : BOLD_TYPEFACE);