Home | History | Annotate | Download | only in speedtracker
      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 
    117         SpeedState(int color) {
    118             mColor = color;
    119         }
    120 
    121         int getColor() {
    122             return mColor;
    123         }
    124     }
    125 
    126     @Override
    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
    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
    214     protected void onResume() {
    215         super.onResume();
    216         if (mGoogleApiClient != null) {
    217             mGoogleApiClient.connect();
    218         }
    219     }
    220 
    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 
    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 
    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      */
    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 
    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
    332     public void onConnected(Bundle bundle) {
    333 
    334         Log.d(TAG, "onConnected()");
    335         requestLocation();
    336 
    337 
    338     }
    339 
    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
    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
    385     public void onConnectionFailed(ConnectionResult connectionResult) {
    386         Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage());
    387     }
    388 
    389     @Override
    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      */
    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
    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
    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      */
    494     private boolean hasGps() {
    495         return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS);
    496     }
    497 }