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.app.Activity; 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.util.Log; 42 import android.view.View; 43 import android.view.WindowManager; 44 import android.widget.ImageButton; 45 import android.widget.ImageView; 46 import android.widget.TextView; 47 48 import com.example.android.wearable.speedtracker.common.Constants; 49 import com.example.android.wearable.speedtracker.common.LocationEntry; 50 import com.example.android.wearable.speedtracker.ui.LocationSettingActivity; 51 52 import java.util.Calendar; 53 54 /** 55 * The main activity for the wearable app. User can pick a speed limit, and after this activity 56 * obtains a fix on the GPS, it starts reporting the speed. In addition to showing the current 57 * speed, if user's speed gets close to the selected speed limit, the color of speed turns yellow 58 * and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS 59 * location data is coming in, a small green dot keeps on blinking while GPS data is available. 60 */ 61 public class WearableMainActivity extends Activity implements GoogleApiClient.ConnectionCallbacks, 62 GoogleApiClient.OnConnectionFailedListener, LocationListener { 63 64 private static final String TAG = "WearableActivity"; 65 66 private static final long UPDATE_INTERVAL_MS = 5 * 1000; 67 private static final long FASTEST_INTERVAL_MS = 5 * 1000; 68 69 public static final float MPH_IN_METERS_PER_SECOND = 2.23694f; 70 71 public static final String PREFS_SPEED_LIMIT_KEY = "speed_limit"; 72 public static final int SPEED_LIMIT_DEFAULT_MPH = 45; 73 private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L; 74 75 private GoogleApiClient mGoogleApiClient; 76 private TextView mSpeedLimitText; 77 private TextView mCurrentSpeedText; 78 private ImageView mSaveImageView; 79 private TextView mAcquiringGps; 80 private TextView mCurrentSpeedMphText; 81 82 private int mCurrentSpeedLimit; 83 private float mCurrentSpeed; 84 private View mDot; 85 private Handler mHandler = new Handler(); 86 private Calendar mCalendar; 87 private boolean mSaveGpsLocation; 88 89 private enum SpeedState { 90 BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above); 91 92 private int mColor; 93 94 SpeedState(int color) { 95 mColor = color; 96 } 97 98 int getColor() { 99 return mColor; 100 } 101 } 102 103 @Override 104 protected void onCreate(Bundle savedInstanceState) { 105 super.onCreate(savedInstanceState); 106 107 setContentView(R.layout.main_activity); 108 if (!hasGps()) { 109 // If this hardware doesn't support GPS, we prefer to exit. 110 // Note that when such device is connected to a phone with GPS capabilities, the 111 // framework automatically routes the location requests to the phone. For this 112 // application, this would not be desirable so we exit the app but for some other 113 // applications, that might be a valid scenario. 114 Log.w(TAG, "This hardware doesn't have GPS, so we exit"); 115 new AlertDialog.Builder(this) 116 .setMessage(getString(R.string.gps_not_available)) 117 .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { 118 @Override 119 public void onClick(DialogInterface dialog, int id) { 120 finish(); 121 dialog.cancel(); 122 } 123 }) 124 .setOnDismissListener(new DialogInterface.OnDismissListener() { 125 @Override 126 public void onDismiss(DialogInterface dialog) { 127 dialog.cancel(); 128 finish(); 129 } 130 }) 131 .setCancelable(false) 132 .create() 133 .show(); 134 } 135 136 setupViews(); 137 updateSpeedVisibility(false); 138 setSpeedLimit(); 139 mGoogleApiClient = new GoogleApiClient.Builder(this) 140 .addApi(LocationServices.API) 141 .addApi(Wearable.API) 142 .addConnectionCallbacks(this) 143 .addOnConnectionFailedListener(this) 144 .build(); 145 mGoogleApiClient.connect(); 146 } 147 148 private void setupViews() { 149 mSpeedLimitText = (TextView) findViewById(R.id.max_speed_text); 150 mCurrentSpeedText = (TextView) findViewById(R.id.current_speed_text); 151 mSaveImageView = (ImageView) findViewById(R.id.saving); 152 ImageButton settingButton = (ImageButton) findViewById(R.id.settings); 153 mAcquiringGps = (TextView) findViewById(R.id.acquiring_gps); 154 mCurrentSpeedMphText = (TextView) findViewById(R.id.current_speed_mph); 155 mDot = findViewById(R.id.dot); 156 157 settingButton.setOnClickListener(new View.OnClickListener() { 158 @Override 159 public void onClick(View v) { 160 Intent speedIntent = new Intent(WearableMainActivity.this, 161 SpeedPickerActivity.class); 162 startActivity(speedIntent); 163 } 164 }); 165 166 mSaveImageView.setOnClickListener(new View.OnClickListener() { 167 @Override 168 public void onClick(View v) { 169 Intent savingIntent = new Intent(WearableMainActivity.this, 170 LocationSettingActivity.class); 171 startActivity(savingIntent); 172 } 173 }); 174 } 175 176 private void setSpeedLimit(int speedLimit) { 177 mSpeedLimitText.setText(getString(R.string.speed_limit, speedLimit)); 178 } 179 180 private void setSpeedLimit() { 181 SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(this); 182 mCurrentSpeedLimit = pref.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH); 183 setSpeedLimit(mCurrentSpeedLimit); 184 } 185 186 private void setCurrentSpeed(float speed) { 187 mCurrentSpeed = speed; 188 mCurrentSpeedText.setText(String.format(getString(R.string.speed_format), speed)); 189 adjustColor(); 190 } 191 192 /** 193 * Adjusts the color of the speed based on its value relative to the speed limit. 194 */ 195 private void adjustColor() { 196 SpeedState state = SpeedState.ABOVE; 197 if (mCurrentSpeed <= mCurrentSpeedLimit - 5) { 198 state = SpeedState.BELOW; 199 } else if (mCurrentSpeed <= mCurrentSpeedLimit) { 200 state = SpeedState.CLOSE; 201 } 202 203 mCurrentSpeedText.setTextColor(getResources().getColor(state.getColor())); 204 } 205 206 @Override 207 public void onConnected(Bundle bundle) { 208 LocationRequest locationRequest = LocationRequest.create() 209 .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) 210 .setInterval(UPDATE_INTERVAL_MS) 211 .setFastestInterval(FASTEST_INTERVAL_MS); 212 213 LocationServices.FusedLocationApi 214 .requestLocationUpdates(mGoogleApiClient, locationRequest, this) 215 .setResultCallback(new ResultCallback<Status>() { 216 217 @Override 218 public void onResult(Status status) { 219 if (status.getStatus().isSuccess()) { 220 if (Log.isLoggable(TAG, Log.DEBUG)) { 221 Log.d(TAG, "Successfully requested location updates"); 222 } 223 } else { 224 Log.e(TAG, 225 "Failed in requesting location updates, " 226 + "status code: " 227 + status.getStatusCode() + ", message: " + status 228 .getStatusMessage()); 229 } 230 } 231 }); 232 } 233 234 @Override 235 public void onConnectionSuspended(int i) { 236 if (Log.isLoggable(TAG, Log.DEBUG)) { 237 Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); 238 } 239 LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); 240 } 241 242 @Override 243 public void onConnectionFailed(ConnectionResult connectionResult) { 244 Log.e(TAG, "onConnectionFailed(): connection to location client failed"); 245 } 246 247 @Override 248 public void onLocationChanged(Location location) { 249 updateSpeedVisibility(true); 250 setCurrentSpeed(location.getSpeed() * MPH_IN_METERS_PER_SECOND); 251 flashDot(); 252 addLocationEntry(location.getLatitude(), location.getLongitude()); 253 } 254 255 /** 256 * Causes the (green) dot blinks when new GPS location data is acquired. 257 */ 258 private void flashDot() { 259 mHandler.post(new Runnable() { 260 @Override 261 public void run() { 262 mDot.setVisibility(View.VISIBLE); 263 } 264 }); 265 mDot.setVisibility(View.VISIBLE); 266 mHandler.postDelayed(new Runnable() { 267 @Override 268 public void run() { 269 mDot.setVisibility(View.INVISIBLE); 270 } 271 }, INDICATOR_DOT_FADE_AWAY_MS); 272 } 273 274 /** 275 * Adjusts the visibility of speed indicator based on the arrival of GPS data. 276 */ 277 private void updateSpeedVisibility(boolean speedVisible) { 278 if (speedVisible) { 279 mAcquiringGps.setVisibility(View.GONE); 280 mCurrentSpeedText.setVisibility(View.VISIBLE); 281 mCurrentSpeedMphText.setVisibility(View.VISIBLE); 282 } else { 283 mAcquiringGps.setVisibility(View.VISIBLE); 284 mCurrentSpeedText.setVisibility(View.GONE); 285 mCurrentSpeedMphText.setVisibility(View.GONE); 286 } 287 } 288 289 /** 290 * Adds a data item to the data Layer storage 291 */ 292 private void addLocationEntry(double latitude, double longitude) { 293 if (!mSaveGpsLocation || !mGoogleApiClient.isConnected()) { 294 return; 295 } 296 mCalendar.setTimeInMillis(System.currentTimeMillis()); 297 LocationEntry entry = new LocationEntry(mCalendar, latitude, longitude); 298 String path = Constants.PATH + "/" + mCalendar.getTimeInMillis(); 299 PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(path); 300 putDataMapRequest.getDataMap().putDouble(Constants.KEY_LATITUDE, entry.latitude); 301 putDataMapRequest.getDataMap().putDouble(Constants.KEY_LONGITUDE, entry.longitude); 302 putDataMapRequest.getDataMap() 303 .putLong(Constants.KEY_TIME, entry.calendar.getTimeInMillis()); 304 PutDataRequest request = putDataMapRequest.asPutDataRequest(); 305 Wearable.DataApi.putDataItem(mGoogleApiClient, request) 306 .setResultCallback(new ResultCallback<DataApi.DataItemResult>() { 307 @Override 308 public void onResult(DataApi.DataItemResult dataItemResult) { 309 if (!dataItemResult.getStatus().isSuccess()) { 310 Log.e(TAG, "AddPoint:onClick(): Failed to set the data, " 311 + "status: " + dataItemResult.getStatus() 312 .getStatusCode()); 313 } 314 } 315 }); 316 } 317 318 @Override 319 protected void onStop() { 320 super.onStop(); 321 if (mGoogleApiClient.isConnected()) { 322 LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); 323 } 324 mGoogleApiClient.disconnect(); 325 } 326 327 @Override 328 protected void onResume() { 329 super.onResume(); 330 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 331 mCalendar = Calendar.getInstance(); 332 setSpeedLimit(); 333 adjustColor(); 334 updateRecordingIcon(); 335 } 336 337 private void updateRecordingIcon() { 338 mSaveGpsLocation = LocationSettingActivity.getGpsRecordingStatusFromPreferences(this); 339 mSaveImageView.setImageResource(mSaveGpsLocation ? R.drawable.ic_gps_saving_grey600_96dp 340 : R.drawable.ic_gps_not_saving_grey600_96dp); 341 } 342 343 /** 344 * Returns {@code true} if this device has the GPS capabilities. 345 */ 346 private boolean hasGps() { 347 return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); 348 } 349 } 350