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.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 }