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