• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2014 Google Inc. All Rights Reserved.
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.speedtracker;
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.ResultCallback;
22 import com.google.android.gms.common.api.Status;
23 import com.google.android.gms.location.LocationListener;
24 import com.google.android.gms.location.LocationRequest;
25 import com.google.android.gms.location.LocationServices;
26 import com.google.android.gms.wearable.DataApi;
27 import com.google.android.gms.wearable.PutDataMapRequest;
28 import com.google.android.gms.wearable.PutDataRequest;
29 import com.google.android.gms.wearable.Wearable;
30 
31 import android.Manifest;
32 import android.app.AlertDialog;
33 import android.content.DialogInterface;
34 import android.content.Intent;
35 import android.content.SharedPreferences;
36 import android.content.pm.PackageManager;
37 import android.location.Location;
38 import android.os.Bundle;
39 import android.os.Handler;
40 import android.preference.PreferenceManager;
41 import android.support.annotation.NonNull;
42 import android.support.v4.app.ActivityCompat;
43 import android.support.wearable.activity.WearableActivity;
44 import android.util.Log;
45 import android.view.View;
46 import android.widget.ImageView;
47 import android.widget.TextView;
48 
49 import com.example.android.wearable.speedtracker.common.Constants;
50 import com.example.android.wearable.speedtracker.common.LocationEntry;
51 
52 import java.util.Calendar;
53 import java.util.concurrent.TimeUnit;
54 
55 /**
56  * The main activity for the wearable app. User can pick a speed limit, and after this activity
57  * obtains a fix on the GPS, it starts reporting the speed. In addition to showing the current
58  * speed, if user's speed gets close to the selected speed limit, the color of speed turns yellow
59  * and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS
60  * location data is coming in, a small green dot keeps on blinking while GPS data is available.
61  */
62 public class WearableMainActivity extends WearableActivity implements
63         GoogleApiClient.ConnectionCallbacks,
64         GoogleApiClient.OnConnectionFailedListener,
65         ActivityCompat.OnRequestPermissionsResultCallback,
66         LocationListener {
67 
68     private static final String TAG = "WearableActivity";
69 
70     private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
71     private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5);
72 
73     private static final float MPH_IN_METERS_PER_SECOND = 2.23694f;
74 
75     private static final int SPEED_LIMIT_DEFAULT_MPH = 45;
76 
77     private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L;
78 
79     // Request codes for changing speed limit and location permissions.
80     private static final int REQUEST_PICK_SPEED_LIMIT = 0;
81 
82     // Id to identify Location permission request.
83     private static final int REQUEST_GPS_PERMISSION = 1;
84 
85     // Shared Preferences for saving speed limit and location permission between app launches.
86     private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit";
87 
88     private Calendar mCalendar;
89 
90     private TextView mSpeedLimitTextView;
91     private TextView mSpeedTextView;
92     private ImageView mGpsPermissionImageView;
93     private TextView mCurrentSpeedMphTextView;
94     private TextView mGpsIssueTextView;
95     private View mBlinkingGpsStatusDotView;
96 
97     private String mGpsPermissionNeededMessage;
98     private String mAcquiringGpsMessage;
99 
100     private int mSpeedLimit;
101     private float mSpeed;
102 
103     private boolean mGpsPermissionApproved;
104 
105     private boolean mWaitingForGpsSignal;
106 
107     private GoogleApiClient mGoogleApiClient;
108 
109     private Handler mHandler = new Handler();
110 
111     private enum SpeedState {
112         BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above);
113 
114         private int mColor;
115 
SpeedState(int color)116         SpeedState(int color) {
117             mColor = color;
118         }
119 
getColor()120         int getColor() {
121             return mColor;
122         }
123     }
124 
125     @Override
onCreate(Bundle savedInstanceState)126     protected void onCreate(Bundle savedInstanceState) {
127         super.onCreate(savedInstanceState);
128 
129         Log.d(TAG, "onCreate()");
130 
131 
132         setContentView(R.layout.main_activity);
133 
134         /*
135          * Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode.
136          * Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to
137          * optimize the display for ambient mode. However, for brevity, we aren't doing that here
138          * to focus on learning location and permissions. For more information on best practices
139          * in ambient mode, check this page:
140          * https://developer.android.com/training/wearables/apps/always-on.html
141          */
142         setAmbientEnabled();
143 
144         mCalendar = Calendar.getInstance();
145 
146         // Enables app to handle 23+ (M+) style permissions.
147         mGpsPermissionApproved =
148             ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
149                     == PackageManager.PERMISSION_GRANTED;
150 
151         mGpsPermissionNeededMessage = getString(R.string.permission_rationale);
152         mAcquiringGpsMessage = getString(R.string.acquiring_gps);
153 
154 
155         SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this);
156         mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH);
157 
158         mSpeed = 0;
159 
160         mWaitingForGpsSignal = true;
161 
162 
163         /*
164          * If this hardware doesn't support GPS, we warn the user. Note that when such device is
165          * connected to a phone with GPS capabilities, the framework automatically routes the
166          * location requests from the phone. However, if the phone becomes disconnected and the
167          * wearable doesn't support GPS, no location is recorded until the phone is reconnected.
168          */
169         if (!hasGps()) {
170             Log.w(TAG, "This hardware doesn't have GPS, so we warn user.");
171             new AlertDialog.Builder(this)
172                     .setMessage(getString(R.string.gps_not_available))
173                     .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
174                         @Override
175                         public void onClick(DialogInterface dialog, int id) {
176                             dialog.cancel();
177                         }
178                     })
179                     .setOnDismissListener(new DialogInterface.OnDismissListener() {
180                         @Override
181                         public void onDismiss(DialogInterface dialog) {
182                             dialog.cancel();
183                         }
184                     })
185                     .setCancelable(false)
186                     .create()
187                     .show();
188         }
189 
190 
191         setupViews();
192 
193         mGoogleApiClient = new GoogleApiClient.Builder(this)
194                 .addApi(LocationServices.API)
195                 .addApi(Wearable.API)
196                 .addConnectionCallbacks(this)
197                 .addOnConnectionFailedListener(this)
198                 .build();
199     }
200 
201     @Override
onPause()202     protected void onPause() {
203         super.onPause();
204         if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) &&
205                 (mGoogleApiClient.isConnecting())) {
206             LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
207             mGoogleApiClient.disconnect();
208         }
209 
210     }
211 
212     @Override
onResume()213     protected void onResume() {
214         super.onResume();
215         if (mGoogleApiClient != null) {
216             mGoogleApiClient.connect();
217         }
218     }
219 
setupViews()220     private void setupViews() {
221         mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text);
222         mSpeedTextView = (TextView) findViewById(R.id.current_speed_text);
223         mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph);
224 
225         mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission);
226         mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text);
227         mBlinkingGpsStatusDotView = findViewById(R.id.dot);
228 
229         updateActivityViewsBasedOnLocationPermissions();
230     }
231 
onSpeedLimitClick(View view)232     public void onSpeedLimitClick(View view) {
233         Intent speedIntent = new Intent(WearableMainActivity.this,
234                 SpeedPickerActivity.class);
235         startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT);
236     }
237 
onGpsPermissionClick(View view)238     public void onGpsPermissionClick(View view) {
239 
240         if (!mGpsPermissionApproved) {
241 
242             Log.i(TAG, "Location permission has NOT been granted. Requesting permission.");
243 
244             // On 23+ (M+) devices, GPS permission not granted. Request permission.
245             ActivityCompat.requestPermissions(
246                     this,
247                     new String[]{Manifest.permission.ACCESS_FINE_LOCATION},
248                     REQUEST_GPS_PERMISSION);
249         }
250     }
251 
252     /**
253      * Adjusts the visibility of views based on location permissions.
254      */
updateActivityViewsBasedOnLocationPermissions()255     private void updateActivityViewsBasedOnLocationPermissions() {
256 
257         /*
258          * If the user has approved location but we don't have a signal yet, we let the user know
259          * we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the
260          * user might think something is wrong.
261          */
262         if (mGpsPermissionApproved && mWaitingForGpsSignal) {
263 
264             // We are getting a GPS signal w/ user permission.
265             mGpsIssueTextView.setText(mAcquiringGpsMessage);
266             mGpsIssueTextView.setVisibility(View.VISIBLE);
267             mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
268 
269             mSpeedTextView.setVisibility(View.GONE);
270             mSpeedLimitTextView.setVisibility(View.GONE);
271             mCurrentSpeedMphTextView.setVisibility(View.GONE);
272 
273         } else if (mGpsPermissionApproved) {
274 
275             mGpsIssueTextView.setVisibility(View.GONE);
276 
277             mSpeedTextView.setVisibility(View.VISIBLE);
278             mSpeedLimitTextView.setVisibility(View.VISIBLE);
279             mCurrentSpeedMphTextView.setVisibility(View.VISIBLE);
280             mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp);
281 
282         } else {
283 
284             // User needs to enable location for the app to work.
285             mGpsIssueTextView.setVisibility(View.VISIBLE);
286             mGpsIssueTextView.setText(mGpsPermissionNeededMessage);
287             mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp);
288 
289             mSpeedTextView.setVisibility(View.GONE);
290             mSpeedLimitTextView.setVisibility(View.GONE);
291             mCurrentSpeedMphTextView.setVisibility(View.GONE);
292         }
293     }
294 
updateSpeedInViews()295     private void updateSpeedInViews() {
296 
297         if (mGpsPermissionApproved) {
298 
299             mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit));
300             mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed));
301 
302             // Adjusts the color of the speed based on its value relative to the speed limit.
303             SpeedState state = SpeedState.ABOVE;
304             if (mSpeed <= mSpeedLimit - 5) {
305                 state = SpeedState.BELOW;
306             } else if (mSpeed <= mSpeedLimit) {
307                 state = SpeedState.CLOSE;
308             }
309 
310             mSpeedTextView.setTextColor(getResources().getColor(state.getColor()));
311 
312             // Causes the (green) dot blinks when new GPS location data is acquired.
313             mHandler.post(new Runnable() {
314                 @Override
315                 public void run() {
316                     mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
317                 }
318             });
319             mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE);
320             mHandler.postDelayed(new Runnable() {
321                 @Override
322                 public void run() {
323                     mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE);
324                 }
325             }, INDICATOR_DOT_FADE_AWAY_MS);
326         }
327     }
328 
329     @Override
onConnected(Bundle bundle)330     public void onConnected(Bundle bundle) {
331 
332         Log.d(TAG, "onConnected()");
333         requestLocation();
334 
335 
336     }
337 
requestLocation()338     private void requestLocation() {
339         Log.d(TAG, "requestLocation()");
340 
341         /*
342          * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or
343          * the device is pre-23, the app uses mSaveGpsLocation to save the user's location
344          * preference.
345          */
346         if (mGpsPermissionApproved) {
347 
348             LocationRequest locationRequest = LocationRequest.create()
349                     .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
350                     .setInterval(UPDATE_INTERVAL_MS)
351                     .setFastestInterval(FASTEST_INTERVAL_MS);
352 
353             LocationServices.FusedLocationApi
354                     .requestLocationUpdates(mGoogleApiClient, locationRequest, this)
355                     .setResultCallback(new ResultCallback<Status>() {
356 
357                         @Override
358                         public void onResult(Status status) {
359                             if (status.getStatus().isSuccess()) {
360                                 if (Log.isLoggable(TAG, Log.DEBUG)) {
361                                     Log.d(TAG, "Successfully requested location updates");
362                                 }
363                             } else {
364                                 Log.e(TAG,
365                                         "Failed in requesting location updates, "
366                                                 + "status code: "
367                                                 + status.getStatusCode() + ", message: " + status
368                                                 .getStatusMessage());
369                             }
370                         }
371                     });
372         }
373     }
374 
375     @Override
onConnectionSuspended(int i)376     public void onConnectionSuspended(int i) {
377         Log.d(TAG, "onConnectionSuspended(): connection to location client suspended");
378 
379         LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this);
380     }
381 
382     @Override
onConnectionFailed(ConnectionResult connectionResult)383     public void onConnectionFailed(ConnectionResult connectionResult) {
384         Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
385     }
386 
387     @Override
onLocationChanged(Location location)388     public void onLocationChanged(Location location) {
389         Log.d(TAG, "onLocationChanged() : " + location);
390 
391 
392         if (mWaitingForGpsSignal) {
393             mWaitingForGpsSignal = false;
394             updateActivityViewsBasedOnLocationPermissions();
395         }
396 
397         mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND;
398         updateSpeedInViews();
399         addLocationEntry(location.getLatitude(), location.getLongitude());
400     }
401 
402     /*
403      * Adds a data item to the data Layer storage.
404      */
addLocationEntry(double latitude, double longitude)405     private void addLocationEntry(double latitude, double longitude) {
406         if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) {
407             return;
408         }
409         mCalendar.setTimeInMillis(System.currentTimeMillis());
410         LocationEntry entry = new LocationEntry(mCalendar, latitude, longitude);
411         String path = Constants.PATH + "/" + mCalendar.getTimeInMillis();
412         PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(path);
413         putDataMapRequest.getDataMap().putDouble(Constants.KEY_LATITUDE, entry.latitude);
414         putDataMapRequest.getDataMap().putDouble(Constants.KEY_LONGITUDE, entry.longitude);
415         putDataMapRequest.getDataMap()
416                 .putLong(Constants.KEY_TIME, entry.calendar.getTimeInMillis());
417         PutDataRequest request = putDataMapRequest.asPutDataRequest();
418         request.setUrgent();
419         Wearable.DataApi.putDataItem(mGoogleApiClient, request)
420                 .setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
421                     @Override
422                     public void onResult(DataApi.DataItemResult dataItemResult) {
423                         if (!dataItemResult.getStatus().isSuccess()) {
424                             Log.e(TAG, "AddPoint:onClick(): Failed to set the data, "
425                                     + "status: " + dataItemResult.getStatus()
426                                     .getStatusCode());
427                         }
428                     }
429                 });
430     }
431 
432     /**
433      * Handles user choices for both speed limit and location permissions (GPS tracking).
434      */
435     @Override
onActivityResult(int requestCode, int resultCode, Intent data)436     protected void onActivityResult(int requestCode, int resultCode, Intent data) {
437 
438         if (requestCode == REQUEST_PICK_SPEED_LIMIT) {
439             if (resultCode == RESULT_OK) {
440                 // The user updated the speed limit.
441                 int newSpeedLimit =
442                         data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit);
443 
444                 SharedPreferences sharedPreferences =
445                         PreferenceManager.getDefaultSharedPreferences(this);
446                 SharedPreferences.Editor editor = sharedPreferences.edit();
447                 editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit);
448                 editor.apply();
449 
450                 mSpeedLimit = newSpeedLimit;
451 
452                 updateSpeedInViews();
453             }
454         }
455     }
456 
457     /**
458      * Callback received when a permissions request has been completed.
459      */
460     @Override
onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults)461     public void onRequestPermissionsResult(
462             int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
463 
464         Log.d(TAG, "onRequestPermissionsResult(): " + permissions);
465 
466 
467         if (requestCode == REQUEST_GPS_PERMISSION) {
468             Log.i(TAG, "Received response for GPS permission request.");
469 
470             if ((grantResults.length == 1)
471                     && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
472                 Log.i(TAG, "GPS permission granted.");
473                 mGpsPermissionApproved = true;
474 
475                 if(mGoogleApiClient != null && mGoogleApiClient.isConnected()) {
476                     requestLocation();
477                 }
478 
479             } else {
480                 Log.i(TAG, "GPS permission NOT granted.");
481                 mGpsPermissionApproved = false;
482             }
483 
484             updateActivityViewsBasedOnLocationPermissions();
485 
486         }
487     }
488 
489     /**
490      * Returns {@code true} if this device has the GPS capabilities.
491      */
hasGps()492     private boolean hasGps() {
493         return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
494     }
495 }