blob: 25f424cac078a048d7a3124d25bc5bfbaba93cdc [file] [log] [blame]
/*
* 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);
}
}