Skip to content

Most visited

Recently visited

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

FitDistanceWatchFaceService.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 android.content.BroadcastReceiver;
20
import android.content.Context;
21
import android.content.Intent;
22
import android.content.IntentFilter;
23
import android.content.res.Resources;
24
import android.graphics.Canvas;
25
import android.graphics.Color;
26
import android.graphics.Paint;
27
import android.graphics.Rect;
28
import android.graphics.Typeface;
29
import android.os.Bundle;
30
import android.os.Handler;
31
import android.os.Message;
32
import android.support.wearable.watchface.CanvasWatchFaceService;
33
import android.support.wearable.watchface.WatchFaceStyle;
34
import android.text.format.DateFormat;
35
import android.util.Log;
36
import android.view.SurfaceHolder;
37
import android.view.WindowInsets;
38
 
39
import com.google.android.gms.common.ConnectionResult;
40
import com.google.android.gms.common.Scopes;
41
import com.google.android.gms.common.api.GoogleApiClient;
42
import com.google.android.gms.common.api.PendingResult;
43
import com.google.android.gms.common.api.ResultCallback;
44
import com.google.android.gms.common.api.Scope;
45
import com.google.android.gms.common.api.Status;
46
import com.google.android.gms.fitness.Fitness;
47
import com.google.android.gms.fitness.FitnessStatusCodes;
48
import com.google.android.gms.fitness.data.DataPoint;
49
import com.google.android.gms.fitness.data.DataType;
50
import com.google.android.gms.fitness.data.Field;
51
import com.google.android.gms.fitness.result.DailyTotalResult;
52
 
53
import java.util.Calendar;
54
import java.util.List;
55
import java.util.TimeZone;
56
import java.util.concurrent.TimeUnit;
57
 
58
/**
59
 * Displays the user's daily distance total via Google Fit. Distance is polled initially when the
60
 * Google API Client successfully connects and once a minute after that via the onTimeTick callback.
61
 * If you want more frequent updates, you will want to add your own  Handler.
62
 *
63
 * Authentication IS a requirement to request distance from Google Fit on Wear. Otherwise, distance
64
 * will always come back as zero (or stay at whatever the distance was prior to you
65
 * de-authorizing watchface). To authenticate and communicate with Google Fit, you must create a
66
 * project in the Google Developers Console, activate the Fitness API, create an OAuth 2.0
67
 * client ID, and register the public certificate from your app's signed APK. More details can be
68
 * found here: https://developers.google.com/fit/android/get-started#step_3_enable_the_fitness_api
69
 *
70
 * In ambient mode, the seconds are replaced with an AM/PM indicator.
71
 *
72
 * On devices with low-bit ambient mode, the text is drawn without anti-aliasing. On devices which
73
 * require burn-in protection, the hours are drawn in normal rather than bold.
74
 *
75
 */
76
public class FitDistanceWatchFaceService extends CanvasWatchFaceService {
77
 
78
    private static final String TAG = "DistanceWatchFace";
79
 
80
    private static final Typeface BOLD_TYPEFACE =
81
            Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
82
    private static final Typeface NORMAL_TYPEFACE =
83
            Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL);
84
 
85
    /**
86
     * Update rate in milliseconds for active mode (non-ambient).
87
     */
88
    private static final long ACTIVE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(1);
89
 
90
    @Override
91
    public Engine onCreateEngine() {
92
        return new Engine();
93
    }
94
 
95
    private class Engine extends CanvasWatchFaceService.Engine implements
96
            GoogleApiClient.ConnectionCallbacks,
97
            GoogleApiClient.OnConnectionFailedListener,
98
            ResultCallback<DailyTotalResult> {
99
 
100
        private static final int BACKGROUND_COLOR = Color.BLACK;
101
        private static final int TEXT_HOURS_MINS_COLOR = Color.WHITE;
102
        private static final int TEXT_SECONDS_COLOR = Color.GRAY;
103
        private static final int TEXT_AM_PM_COLOR = Color.GRAY;
104
        private static final int TEXT_COLON_COLOR = Color.GRAY;
105
        private static final int TEXT_DISTANCE_COUNT_COLOR = Color.GRAY;
106
 
107
        private static final String COLON_STRING = ":";
108
 
109
        private static final int MSG_UPDATE_TIME = 0;
110
 
111
        /* Handler to update the time periodically in interactive mode. */
112
        private final Handler mUpdateTimeHandler = new Handler() {
113
            @Override
114
            public void handleMessage(Message message) {
115
                switch (message.what) {
116
                    case MSG_UPDATE_TIME:
117
                        Log.v(TAG, "updating time");
118
                        invalidate();
119
                        if (shouldUpdateTimeHandlerBeRunning()) {
120
                            long timeMs = System.currentTimeMillis();
121
                            long delayMs =
122
                                    ACTIVE_INTERVAL_MS - (timeMs % ACTIVE_INTERVAL_MS);
123
                            mUpdateTimeHandler.sendEmptyMessageDelayed(MSG_UPDATE_TIME, delayMs);
124
                        }
125
                        break;
126
                }
127
            }
128
        };
129
 
130
        /**
131
         * Handles time zone and locale changes.
132
         */
133
        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
134
            @Override
135
            public void onReceive(Context context, Intent intent) {
136
                mCalendar.setTimeZone(TimeZone.getDefault());
137
                invalidate();
138
            }
139
        };
140
 
141
        /**
142
         * Unregistering an unregistered receiver throws an exception. Keep track of the
143
         * registration state to prevent that.
144
         */
145
        private boolean mRegisteredReceiver = false;
146
 
147
        private Paint mHourPaint;
148
        private Paint mMinutePaint;
149
        private Paint mSecondPaint;
150
        private Paint mAmPmPaint;
151
        private Paint mColonPaint;
152
        private Paint mDistanceCountPaint;
153
 
154
        private float mColonWidth;
155
 
156
        private Calendar mCalendar;
157
 
158
        private float mXOffset;
159
        private float mXDistanceOffset;
160
        private float mYOffset;
161
        private float mLineHeight;
162
 
163
        private String mAmString;
164
        private String mPmString;
165
 
166
 
167
        /**
168
         * Whether the display supports fewer bits for each color in ambient mode. When true, we
169
         * disable anti-aliasing in ambient mode.
170
         */
171
        private boolean mLowBitAmbient;
172
 
173
        /*
174
         * Google API Client used to make Google Fit requests for step data.
175
         */
176
        private GoogleApiClient mGoogleApiClient;
177
 
178
        private boolean mDistanceRequested;
179
 
180
        private float mDistanceTotal = 0;
181
 
182
        @Override
183
        public void onCreate(SurfaceHolder holder) {
184
            Log.d(TAG, "onCreate");
185
 
186
            super.onCreate(holder);
187
 
188
            mDistanceRequested = false;
189
            mGoogleApiClient = new GoogleApiClient.Builder(FitDistanceWatchFaceService.this)
190
                    .addConnectionCallbacks(this)
191
                    .addOnConnectionFailedListener(this)
192
                    .addApi(Fitness.HISTORY_API)
193
                    .addApi(Fitness.RECORDING_API)
194
                    .addScope(new Scope(Scopes.FITNESS_LOCATION_READ))
195
                    // When user has multiple accounts, useDefaultAccount() allows Google Fit to
196
                    // associated with the main account for steps. It also replaces the need for
197
                    // a scope request.
198
                    .useDefaultAccount()
199
                    .build();
200
 
201
            setWatchFaceStyle(new WatchFaceStyle.Builder(FitDistanceWatchFaceService.this)
202
                    .setCardPeekMode(WatchFaceStyle.PEEK_MODE_VARIABLE)
203
                    .setBackgroundVisibility(WatchFaceStyle.BACKGROUND_VISIBILITY_INTERRUPTIVE)
204
                    .setShowSystemUiTime(false)
205
                    .build());
206
 
207
            Resources resources = getResources();
208
 
209
            mYOffset = resources.getDimension(R.dimen.fit_y_offset);
210
            mLineHeight = resources.getDimension(R.dimen.fit_line_height);
211
            mAmString = resources.getString(R.string.fit_am);
212
            mPmString = resources.getString(R.string.fit_pm);
213
 
214
            mHourPaint = createTextPaint(TEXT_HOURS_MINS_COLOR, BOLD_TYPEFACE);
215
            mMinutePaint = createTextPaint(TEXT_HOURS_MINS_COLOR);
216
            mSecondPaint = createTextPaint(TEXT_SECONDS_COLOR);
217
            mAmPmPaint = createTextPaint(TEXT_AM_PM_COLOR);
218
            mColonPaint = createTextPaint(TEXT_COLON_COLOR);
219
            mDistanceCountPaint = createTextPaint(TEXT_DISTANCE_COUNT_COLOR);
220
 
221
            mCalendar = Calendar.getInstance();
222
 
223
        }
224
 
225
        @Override
226
        public void onDestroy() {
227
            mUpdateTimeHandler.removeMessages(MSG_UPDATE_TIME);
228
            super.onDestroy();
229
        }
230
 
231
        private Paint createTextPaint(int color) {
232
            return createTextPaint(color, NORMAL_TYPEFACE);
233
        }
234
 
235
        private Paint createTextPaint(int color, Typeface typeface) {
236
            Paint paint = new Paint();
237
            paint.setColor(color);
238
            paint.setTypeface(typeface);
239
            paint.setAntiAlias(true);
240
            return paint;
241
        }
242
 
243
        @Override
244
        public void onVisibilityChanged(boolean visible) {
245
            Log.d(TAG, "onVisibilityChanged: " + visible);
246
 
247
            super.onVisibilityChanged(visible);
248
 
249
            if (visible) {
250
                mGoogleApiClient.connect();
251
 
252
                registerReceiver();
253
 
254
                // Update time zone and date formats, in case they changed while we weren't visible.
255
                mCalendar.setTimeZone(TimeZone.getDefault());
256
            } else {
257
                unregisterReceiver();
258
 
259
                if (mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
260
                    mGoogleApiClient.disconnect();
261
                }
262
            }
263
 
264
            // Whether the timer should be running depends on whether we're visible (as well as
265
            // whether we're in ambient mode), so we may need to start or stop the timer.
266
            updateTimer();
267
        }
268
 
269
 
270
        private void registerReceiver() {
271
            if (mRegisteredReceiver) {
272
                return;
273
            }
274
            mRegisteredReceiver = true;
275
            IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_CHANGED);
276
            FitDistanceWatchFaceService.this.registerReceiver(mReceiver, filter);
277
        }
278
 
279
        private void unregisterReceiver() {
280
            if (!mRegisteredReceiver) {
281
                return;
282
            }
283
            mRegisteredReceiver = false;
284
            FitDistanceWatchFaceService.this.unregisterReceiver(mReceiver);
285
        }
286
 
287
        @Override
288
        public void onApplyWindowInsets(WindowInsets insets) {
289
            Log.d(TAG, "onApplyWindowInsets: " + (insets.isRound() ? "round" : "square"));
290
 
291
            super.onApplyWindowInsets(insets);
292
 
293
            // Load resources that have alternate values for round watches.
294
            Resources resources = FitDistanceWatchFaceService.this.getResources();
295
            boolean isRound = insets.isRound();
296
            mXOffset = resources.getDimension(isRound
297
                    ? R.dimen.fit_x_offset_round : R.dimen.fit_x_offset);
298
            mXDistanceOffset =
299
                    resources.getDimension(
300
                            isRound ?
301
                            R.dimen.fit_steps_or_distance_x_offset_round :
302