| /* |
| * Copyright (C) 2014 Google Inc. All Rights Reserved. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.example.android.wearable.speedtracker; |
| |
| import com.google.android.gms.common.ConnectionResult; |
| import com.google.android.gms.common.api.GoogleApiClient; |
| import com.google.android.gms.common.api.ResultCallback; |
| import com.google.android.gms.common.api.Status; |
| import com.google.android.gms.location.LocationListener; |
| import com.google.android.gms.location.LocationRequest; |
| import com.google.android.gms.location.LocationServices; |
| import com.google.android.gms.wearable.DataApi; |
| import com.google.android.gms.wearable.PutDataMapRequest; |
| import com.google.android.gms.wearable.PutDataRequest; |
| import com.google.android.gms.wearable.Wearable; |
| |
| import android.Manifest; |
| import android.app.AlertDialog; |
| import android.content.DialogInterface; |
| import android.content.Intent; |
| import android.content.SharedPreferences; |
| import android.content.pm.PackageManager; |
| import android.location.Location; |
| import android.os.Bundle; |
| import android.os.Handler; |
| import android.preference.PreferenceManager; |
| import android.support.annotation.NonNull; |
| import android.support.v4.app.ActivityCompat; |
| import android.support.wearable.activity.WearableActivity; |
| import android.util.Log; |
| import android.view.View; |
| import android.widget.ImageView; |
| import android.widget.TextView; |
| |
| import com.example.android.wearable.speedtracker.common.Constants; |
| import com.example.android.wearable.speedtracker.common.LocationEntry; |
| |
| import java.util.Calendar; |
| import java.util.concurrent.TimeUnit; |
| |
| /** |
| * The main activity for the wearable app. User can pick a speed limit, and after this activity |
| * obtains a fix on the GPS, it starts reporting the speed. In addition to showing the current |
| * speed, if user's speed gets close to the selected speed limit, the color of speed turns yellow |
| * and if the user exceeds the speed limit, it will turn red. In order to show the user that GPS |
| * location data is coming in, a small green dot keeps on blinking while GPS data is available. |
| */ |
| public class WearableMainActivity extends WearableActivity implements |
| GoogleApiClient.ConnectionCallbacks, |
| GoogleApiClient.OnConnectionFailedListener, |
| ActivityCompat.OnRequestPermissionsResultCallback, |
| LocationListener { |
| |
| private static final String TAG = "WearableActivity"; |
| |
| private static final long UPDATE_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); |
| private static final long FASTEST_INTERVAL_MS = TimeUnit.SECONDS.toMillis(5); |
| |
| private static final float MPH_IN_METERS_PER_SECOND = 2.23694f; |
| |
| private static final int SPEED_LIMIT_DEFAULT_MPH = 45; |
| |
| private static final long INDICATOR_DOT_FADE_AWAY_MS = 500L; |
| |
| // Request codes for changing speed limit and location permissions. |
| private static final int REQUEST_PICK_SPEED_LIMIT = 0; |
| |
| // Id to identify Location permission request. |
| private static final int REQUEST_GPS_PERMISSION = 1; |
| |
| // Shared Preferences for saving speed limit and location permission between app launches. |
| private static final String PREFS_SPEED_LIMIT_KEY = "SpeedLimit"; |
| |
| private Calendar mCalendar; |
| |
| private TextView mSpeedLimitTextView; |
| private TextView mSpeedTextView; |
| private ImageView mGpsPermissionImageView; |
| private TextView mCurrentSpeedMphTextView; |
| private TextView mGpsIssueTextView; |
| private View mBlinkingGpsStatusDotView; |
| |
| private String mGpsPermissionNeededMessage; |
| private String mAcquiringGpsMessage; |
| |
| private int mSpeedLimit; |
| private float mSpeed; |
| |
| private boolean mGpsPermissionApproved; |
| |
| private boolean mWaitingForGpsSignal; |
| |
| private GoogleApiClient mGoogleApiClient; |
| |
| private Handler mHandler = new Handler(); |
| |
| private enum SpeedState { |
| BELOW(R.color.speed_below), CLOSE(R.color.speed_close), ABOVE(R.color.speed_above); |
| |
| private int mColor; |
| |
| SpeedState(int color) { |
| mColor = color; |
| } |
| |
| int getColor() { |
| return mColor; |
| } |
| } |
| |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| |
| Log.d(TAG, "onCreate()"); |
| |
| |
| setContentView(R.layout.main_activity); |
| |
| /* |
| * Enables Always-on, so our app doesn't shut down when the watch goes into ambient mode. |
| * Best practice is to override onEnterAmbient(), onUpdateAmbient(), and onExitAmbient() to |
| * optimize the display for ambient mode. However, for brevity, we aren't doing that here |
| * to focus on learning location and permissions. For more information on best practices |
| * in ambient mode, check this page: |
| * https://developer.android.com/training/wearables/apps/always-on.html |
| */ |
| setAmbientEnabled(); |
| |
| mCalendar = Calendar.getInstance(); |
| |
| // Enables app to handle 23+ (M+) style permissions. |
| mGpsPermissionApproved = |
| ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) |
| == PackageManager.PERMISSION_GRANTED; |
| |
| mGpsPermissionNeededMessage = getString(R.string.permission_rationale); |
| mAcquiringGpsMessage = getString(R.string.acquiring_gps); |
| |
| |
| SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); |
| mSpeedLimit = sharedPreferences.getInt(PREFS_SPEED_LIMIT_KEY, SPEED_LIMIT_DEFAULT_MPH); |
| |
| mSpeed = 0; |
| |
| mWaitingForGpsSignal = true; |
| |
| |
| /* |
| * If this hardware doesn't support GPS, we warn the user. Note that when such device is |
| * connected to a phone with GPS capabilities, the framework automatically routes the |
| * location requests from the phone. However, if the phone becomes disconnected and the |
| * wearable doesn't support GPS, no location is recorded until the phone is reconnected. |
| */ |
| if (!hasGps()) { |
| Log.w(TAG, "This hardware doesn't have GPS, so we warn user."); |
| new AlertDialog.Builder(this) |
| .setMessage(getString(R.string.gps_not_available)) |
| .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { |
| @Override |
| public void onClick(DialogInterface dialog, int id) { |
| dialog.cancel(); |
| } |
| }) |
| .setOnDismissListener(new DialogInterface.OnDismissListener() { |
| @Override |
| public void onDismiss(DialogInterface dialog) { |
| dialog.cancel(); |
| } |
| }) |
| .setCancelable(false) |
| .create() |
| .show(); |
| } |
| |
| |
| setupViews(); |
| |
| mGoogleApiClient = new GoogleApiClient.Builder(this) |
| .addApi(LocationServices.API) |
| .addApi(Wearable.API) |
| .addConnectionCallbacks(this) |
| .addOnConnectionFailedListener(this) |
| .build(); |
| } |
| |
| @Override |
| protected void onPause() { |
| super.onPause(); |
| if ((mGoogleApiClient != null) && (mGoogleApiClient.isConnected()) && |
| (mGoogleApiClient.isConnecting())) { |
| LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); |
| mGoogleApiClient.disconnect(); |
| } |
| |
| } |
| |
| @Override |
| protected void onResume() { |
| super.onResume(); |
| if (mGoogleApiClient != null) { |
| mGoogleApiClient.connect(); |
| } |
| } |
| |
| private void setupViews() { |
| mSpeedLimitTextView = (TextView) findViewById(R.id.max_speed_text); |
| mSpeedTextView = (TextView) findViewById(R.id.current_speed_text); |
| mCurrentSpeedMphTextView = (TextView) findViewById(R.id.current_speed_mph); |
| |
| mGpsPermissionImageView = (ImageView) findViewById(R.id.gps_permission); |
| mGpsIssueTextView = (TextView) findViewById(R.id.gps_issue_text); |
| mBlinkingGpsStatusDotView = findViewById(R.id.dot); |
| |
| updateActivityViewsBasedOnLocationPermissions(); |
| } |
| |
| public void onSpeedLimitClick(View view) { |
| Intent speedIntent = new Intent(WearableMainActivity.this, |
| SpeedPickerActivity.class); |
| startActivityForResult(speedIntent, REQUEST_PICK_SPEED_LIMIT); |
| } |
| |
| public void onGpsPermissionClick(View view) { |
| |
| if (!mGpsPermissionApproved) { |
| |
| Log.i(TAG, "Location permission has NOT been granted. Requesting permission."); |
| |
| // On 23+ (M+) devices, GPS permission not granted. Request permission. |
| ActivityCompat.requestPermissions( |
| this, |
| new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, |
| REQUEST_GPS_PERMISSION); |
| } |
| } |
| |
| /** |
| * Adjusts the visibility of views based on location permissions. |
| */ |
| private void updateActivityViewsBasedOnLocationPermissions() { |
| |
| /* |
| * If the user has approved location but we don't have a signal yet, we let the user know |
| * we are waiting on the GPS signal (this sometimes takes a little while). Otherwise, the |
| * user might think something is wrong. |
| */ |
| if (mGpsPermissionApproved && mWaitingForGpsSignal) { |
| |
| // We are getting a GPS signal w/ user permission. |
| mGpsIssueTextView.setText(mAcquiringGpsMessage); |
| mGpsIssueTextView.setVisibility(View.VISIBLE); |
| mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp); |
| |
| mSpeedTextView.setVisibility(View.GONE); |
| mSpeedLimitTextView.setVisibility(View.GONE); |
| mCurrentSpeedMphTextView.setVisibility(View.GONE); |
| |
| } else if (mGpsPermissionApproved) { |
| |
| mGpsIssueTextView.setVisibility(View.GONE); |
| |
| mSpeedTextView.setVisibility(View.VISIBLE); |
| mSpeedLimitTextView.setVisibility(View.VISIBLE); |
| mCurrentSpeedMphTextView.setVisibility(View.VISIBLE); |
| mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_saving_grey600_96dp); |
| |
| } else { |
| |
| // User needs to enable location for the app to work. |
| mGpsIssueTextView.setVisibility(View.VISIBLE); |
| mGpsIssueTextView.setText(mGpsPermissionNeededMessage); |
| mGpsPermissionImageView.setImageResource(R.drawable.ic_gps_not_saving_grey600_96dp); |
| |
| mSpeedTextView.setVisibility(View.GONE); |
| mSpeedLimitTextView.setVisibility(View.GONE); |
| mCurrentSpeedMphTextView.setVisibility(View.GONE); |
| } |
| } |
| |
| private void updateSpeedInViews() { |
| |
| if (mGpsPermissionApproved) { |
| |
| mSpeedLimitTextView.setText(getString(R.string.speed_limit, mSpeedLimit)); |
| mSpeedTextView.setText(String.format(getString(R.string.speed_format), mSpeed)); |
| |
| // Adjusts the color of the speed based on its value relative to the speed limit. |
| SpeedState state = SpeedState.ABOVE; |
| if (mSpeed <= mSpeedLimit - 5) { |
| state = SpeedState.BELOW; |
| } else if (mSpeed <= mSpeedLimit) { |
| state = SpeedState.CLOSE; |
| } |
| |
| mSpeedTextView.setTextColor(getResources().getColor(state.getColor())); |
| |
| // Causes the (green) dot blinks when new GPS location data is acquired. |
| mHandler.post(new Runnable() { |
| @Override |
| public void run() { |
| mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE); |
| } |
| }); |
| mBlinkingGpsStatusDotView.setVisibility(View.VISIBLE); |
| mHandler.postDelayed(new Runnable() { |
| @Override |
| public void run() { |
| mBlinkingGpsStatusDotView.setVisibility(View.INVISIBLE); |
| } |
| }, INDICATOR_DOT_FADE_AWAY_MS); |
| } |
| } |
| |
| @Override |
| public void onConnected(Bundle bundle) { |
| |
| Log.d(TAG, "onConnected()"); |
| |
| /* |
| * mGpsPermissionApproved covers 23+ (M+) style permissions. If that is already approved or |
| * the device is pre-23, the app uses mSaveGpsLocation to save the user's location |
| * preference. |
| */ |
| if (mGpsPermissionApproved) { |
| |
| LocationRequest locationRequest = LocationRequest.create() |
| .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY) |
| .setInterval(UPDATE_INTERVAL_MS) |
| .setFastestInterval(FASTEST_INTERVAL_MS); |
| |
| LocationServices.FusedLocationApi |
| .requestLocationUpdates(mGoogleApiClient, locationRequest, this) |
| .setResultCallback(new ResultCallback<Status>() { |
| |
| @Override |
| public void onResult(Status status) { |
| if (status.getStatus().isSuccess()) { |
| if (Log.isLoggable(TAG, Log.DEBUG)) { |
| Log.d(TAG, "Successfully requested location updates"); |
| } |
| } else { |
| Log.e(TAG, |
| "Failed in requesting location updates, " |
| + "status code: " |
| + status.getStatusCode() + ", message: " + status |
| .getStatusMessage()); |
| } |
| } |
| }); |
| } |
| } |
| |
| @Override |
| public void onConnectionSuspended(int i) { |
| Log.d(TAG, "onConnectionSuspended(): connection to location client suspended"); |
| |
| LocationServices.FusedLocationApi.removeLocationUpdates(mGoogleApiClient, this); |
| } |
| |
| @Override |
| public void onConnectionFailed(ConnectionResult connectionResult) { |
| Log.e(TAG, "onConnectionFailed(): " + connectionResult.getErrorMessage()); |
| } |
| |
| @Override |
| public void onLocationChanged(Location location) { |
| Log.d(TAG, "onLocationChanged() : " + location); |
| |
| |
| if (mWaitingForGpsSignal) { |
| mWaitingForGpsSignal = false; |
| updateActivityViewsBasedOnLocationPermissions(); |
| } |
| |
| mSpeed = location.getSpeed() * MPH_IN_METERS_PER_SECOND; |
| updateSpeedInViews(); |
| addLocationEntry(location.getLatitude(), location.getLongitude()); |
| } |
| |
| /* |
| * Adds a data item to the data Layer storage. |
| */ |
| private void addLocationEntry(double latitude, double longitude) { |
| if (!mGpsPermissionApproved || !mGoogleApiClient.isConnected()) { |
| return; |
| } |
| mCalendar.setTimeInMillis(System.currentTimeMillis()); |
| LocationEntry entry = new LocationEntry(mCalendar, latitude, longitude); |
| String path = Constants.PATH + "/" + mCalendar.getTimeInMillis(); |
| PutDataMapRequest putDataMapRequest = PutDataMapRequest.create(path); |
| putDataMapRequest.getDataMap().putDouble(Constants.KEY_LATITUDE, entry.latitude); |
| putDataMapRequest.getDataMap().putDouble(Constants.KEY_LONGITUDE, entry.longitude); |
| putDataMapRequest.getDataMap() |
| .putLong(Constants.KEY_TIME, entry.calendar.getTimeInMillis()); |
| PutDataRequest request = putDataMapRequest.asPutDataRequest(); |
| request.setUrgent(); |
| Wearable.DataApi.putDataItem(mGoogleApiClient, request) |
| .setResultCallback(new ResultCallback<DataApi.DataItemResult>() { |
| @Override |
| public void onResult(DataApi.DataItemResult dataItemResult) { |
| if (!dataItemResult.getStatus().isSuccess()) { |
| Log.e(TAG, "AddPoint:onClick(): Failed to set the data, " |
| + "status: " + dataItemResult.getStatus() |
| .getStatusCode()); |
| } |
| } |
| }); |
| } |
| |
| /** |
| * Handles user choices for both speed limit and location permissions (GPS tracking). |
| */ |
| @Override |
| protected void onActivityResult(int requestCode, int resultCode, Intent data) { |
| |
| if (requestCode == REQUEST_PICK_SPEED_LIMIT) { |
| if (resultCode == RESULT_OK) { |
| // The user updated the speed limit. |
| int newSpeedLimit = |
| data.getIntExtra(SpeedPickerActivity.EXTRA_NEW_SPEED_LIMIT, mSpeedLimit); |
| |
| SharedPreferences sharedPreferences = |
| PreferenceManager.getDefaultSharedPreferences(this); |
| SharedPreferences.Editor editor = sharedPreferences.edit(); |
| editor.putInt(WearableMainActivity.PREFS_SPEED_LIMIT_KEY, newSpeedLimit); |
| editor.apply(); |
| |
| mSpeedLimit = newSpeedLimit; |
| |
| updateSpeedInViews(); |
| } |
| } |
| } |
| |
| /** |
| * Callback received when a permissions request has been completed. |
| */ |
| @Override |
| public void onRequestPermissionsResult( |
| int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { |
| |
| Log.d(TAG, "onRequestPermissionsResult(): " + permissions); |
| |
| |
| if (requestCode == REQUEST_GPS_PERMISSION) { |
| Log.i(TAG, "Received response for GPS permission request."); |
| |
| if ((grantResults.length == 1) |
| && (grantResults[0] == PackageManager.PERMISSION_GRANTED)) { |
| Log.i(TAG, "GPS permission granted."); |
| mGpsPermissionApproved = true; |
| } else { |
| Log.i(TAG, "GPS permission NOT granted."); |
| mGpsPermissionApproved = false; |
| } |
| |
| updateActivityViewsBasedOnLocationPermissions(); |
| |
| } |
| } |
| |
| /** |
| * Returns {@code true} if this device has the GPS capabilities. |
| */ |
| private boolean hasGps() { |
| return getPackageManager().hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS); |
| } |
| } |