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 }