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 116 SpeedState(int color) { 117 mColor = color; 118 } 119 120 int getColor() { 121 return mColor; 122 } 123 } 124 125 @Override 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 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 213 protected void onResume() { 214 super.onResume(); 215 if (mGoogleApiClient != null) { 216 mGoogleApiClient.connect(); 217 } 218 } 219 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 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 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 */ 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 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 330 public void onConnected(Bundle bundle) { 331 332 Log.d(TAG, "onConnected()"); 333 requestLocation(); 334 335 336 } 337 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 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 383 public void onConnectionFailed(ConnectionResult connectionResult) { 384 Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage()); 385 } 386 387 @Override 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 */ 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 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 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 */ 492 private boolean hasGps() { 493 return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); 494 } 495 }