forked from upstream-mirrors/OpenTracks
Fix an issue when run SyncTest.
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is a simple location listener policy that will always dictate the same
|
||||
* polling interval.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AbsoluteLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param interval the interval to request for gps signal
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getDesiredPollingInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIdleTime(long idleTime) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.gms.location.ActivityRecognitionResult;
|
||||
import com.google.android.gms.location.DetectedActivity;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Service that receives ActivityRecognition updates.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ActivityRecognitionIntentService extends IntentService {
|
||||
|
||||
private static final int REQUIRED_CONFIDENCE = 90;
|
||||
|
||||
public ActivityRecognitionIntentService() {
|
||||
super(ActivityRecognitionIntentService.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
if (!ActivityRecognitionResult.hasResult(intent)) {
|
||||
return;
|
||||
}
|
||||
ActivityRecognitionResult result = ActivityRecognitionResult.extractResult(intent);
|
||||
DetectedActivity detectedActivity = result.getMostProbableActivity();
|
||||
if (detectedActivity == null) {
|
||||
return;
|
||||
}
|
||||
if (detectedActivity.getConfidence() < REQUIRED_CONFIDENCE) {
|
||||
return;
|
||||
}
|
||||
|
||||
long recordingTrackId = PreferencesUtils.getLong(this, R.string.recording_track_id_key);
|
||||
if (recordingTrackId == PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
return;
|
||||
}
|
||||
boolean recordingTrackPaused = PreferencesUtils.getBoolean(
|
||||
this, R.string.recording_track_paused_key, PreferencesUtils.RECORDING_TRACK_PAUSED_DEFAULT);
|
||||
if (recordingTrackPaused) {
|
||||
return;
|
||||
}
|
||||
|
||||
int currentType = PreferencesUtils.getInt(this, R.string.activity_recognition_type_key,
|
||||
PreferencesUtils.ACTIVITY_RECOGNITION_TYPE_DEFAULT);
|
||||
|
||||
switch (detectedActivity.getType()) {
|
||||
case DetectedActivity.IN_VEHICLE:
|
||||
if (currentType != DetectedActivity.IN_VEHICLE) {
|
||||
PreferencesUtils.setInt(
|
||||
this, R.string.activity_recognition_type_key, DetectedActivity.IN_VEHICLE);
|
||||
}
|
||||
break;
|
||||
case DetectedActivity.ON_BICYCLE:
|
||||
if (currentType != DetectedActivity.IN_VEHICLE
|
||||
&& currentType != DetectedActivity.ON_BICYCLE) {
|
||||
PreferencesUtils.setInt(
|
||||
this, R.string.activity_recognition_type_key, DetectedActivity.ON_BICYCLE);
|
||||
}
|
||||
break;
|
||||
case DetectedActivity.ON_FOOT:
|
||||
if (currentType != DetectedActivity.IN_VEHICLE && currentType != DetectedActivity.ON_BICYCLE
|
||||
&& currentType != DetectedActivity.ON_FOOT) {
|
||||
PreferencesUtils.setInt(
|
||||
this, R.string.activity_recognition_type_key, DetectedActivity.ON_FOOT);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* A {@link LocationListenerPolicy} that will change based on how long the user
|
||||
* has been stationary. This policy will dictate a policy based on a min, max
|
||||
* and idle time. The policy will dictate an interval bounded by min and max,
|
||||
* and is half of the idle time.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
private final long minInterval;
|
||||
private final long maxInterval;
|
||||
private final int minDistance;
|
||||
|
||||
// The time the user has been idle at the current location, in milliseconds.
|
||||
private long idleTime;
|
||||
|
||||
/**
|
||||
* Creates a policy that will be bounded by the given minInterval and
|
||||
* maxInterval.
|
||||
*
|
||||
* @param minInterval the smallest interval this policy will dictate, in
|
||||
* milliseconds
|
||||
* @param maxInterval the largest interval this policy will dictate, in
|
||||
* milliseconds
|
||||
* @param minDistance the minimum distance in meters
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long minInterval, long maxInterval, int minDistance) {
|
||||
this.minInterval = minInterval;
|
||||
this.maxInterval = maxInterval;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns an interval half of the idle time, but bounded by minInteval and
|
||||
* maxInterval.
|
||||
*/
|
||||
@Override
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to second to avoid setting the interval too often
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval), minInterval);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
idleTime = newIdleTime;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.widgets.TrackWidgetProvider;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.app.IntentService;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A service to control starting and stopping of a recording. This service,
|
||||
* through the AndroidManifest.xml, is configured to only allow components of
|
||||
* the same application to invoke it. Thus this service can be used my MyTracks
|
||||
* app widget, {@link TrackWidgetProvider}, but not by other applications. This
|
||||
* application delegates starting and stopping a recording to
|
||||
* {@link TrackRecordingService} using RPC calls.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ControlRecordingService extends IntentService implements ServiceConnection {
|
||||
|
||||
private static final String TAG = ControlRecordingService.class.getSimpleName();
|
||||
|
||||
private ITrackRecordingService trackRecordingService;
|
||||
private boolean connected = false;
|
||||
|
||||
public ControlRecordingService() {
|
||||
super(ControlRecordingService.class.getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
Intent newIntent = new Intent(this, TrackRecordingService.class);
|
||||
startService(newIntent);
|
||||
bindService(newIntent, this, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName name, IBinder service) {
|
||||
trackRecordingService = ITrackRecordingService.Stub.asInterface(service);
|
||||
notifyConnected();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName name) {
|
||||
connected = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies all threads that connection to {@link TrackRecordingService} is
|
||||
* available.
|
||||
*/
|
||||
private synchronized void notifyConnected() {
|
||||
connected = true;
|
||||
notifyAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the connection to {@link TrackRecordingService} is available.
|
||||
*/
|
||||
private synchronized void waitConnected() {
|
||||
while (!connected) {
|
||||
try {
|
||||
wait();
|
||||
} catch (InterruptedException e) {
|
||||
|
||||
// can safely ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
waitConnected();
|
||||
onHandleIntent(intent, trackRecordingService);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the intent to start or stop a recording.
|
||||
*
|
||||
* @param intent to be handled
|
||||
* @param service the trackRecordingService
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onHandleIntent(Intent intent, ITrackRecordingService service) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
try {
|
||||
if (action.equals(getString(R.string.track_action_start))) {
|
||||
service.startNewTrack();
|
||||
} else if (action.equals(getString(R.string.track_action_end))) {
|
||||
service.endCurrentTrack();
|
||||
} else if (action.equals(getString(R.string.track_action_pause))) {
|
||||
service.pauseCurrentTrack();
|
||||
} else if (action.equals(getString(R.string.track_action_resume))) {
|
||||
service.resumeCurrentTrack();
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
Log.d(TAG, "ControlRecordingService onHandleIntent RemoteException", e);
|
||||
}
|
||||
}
|
||||
unbindService(this);
|
||||
connected = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (connected) {
|
||||
unbindService(this);
|
||||
connected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
/**
|
||||
* This is an interface for classes that will manage the location listener
|
||||
* policy. Different policy options are: {@link AbsoluteLocationListenerPolicy}
|
||||
* and {@link AdaptiveLocationListenerPolicy}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling interval this policy would like at this moment.
|
||||
*
|
||||
* @return the polling interval
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance();
|
||||
|
||||
/**
|
||||
* Notifies the amount of time the user has been idle at his current location.
|
||||
*
|
||||
* @param idleTime the time that the user has been idle at his current
|
||||
* location
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.util.GoogleLocationUtils;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GooglePlayServicesClient.ConnectionCallbacks;
|
||||
import com.google.android.gms.common.GooglePlayServicesClient.OnConnectionFailedListener;
|
||||
import com.google.android.gms.location.LocationClient;
|
||||
import com.google.android.gms.location.LocationListener;
|
||||
import com.google.android.gms.location.LocationRequest;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* My Tracks Location Manager. Applies Google location settings before allowing
|
||||
* access to {@link LocationManager}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class MyTracksLocationManager {
|
||||
|
||||
/**
|
||||
* Observer for Google location settings.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
private class GoogleSettingsObserver extends ContentObserver {
|
||||
|
||||
public GoogleSettingsObserver(Handler handler) {
|
||||
super(handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
isAllowed = isUseLocationForServicesOn();
|
||||
}
|
||||
}
|
||||
|
||||
private static final String TAG = MyTracksLocationManager.class.getSimpleName();
|
||||
|
||||
private static final String GOOGLE_SETTINGS_CONTENT_URI = "content://com.google.settings/partner";
|
||||
private static final String USE_LOCATION_FOR_SERVICES = "use_location_for_services";
|
||||
|
||||
// User has agreed to use location for Google services.
|
||||
@VisibleForTesting
|
||||
static final String USE_LOCATION_FOR_SERVICES_ON = "1";
|
||||
|
||||
private static final String NAME = "name";
|
||||
private static final String VALUE = "value";
|
||||
|
||||
private final ConnectionCallbacks connectionCallbacks = new ConnectionCallbacks() {
|
||||
@Override
|
||||
public void onDisconnected() {}
|
||||
|
||||
@Override
|
||||
public void onConnected(Bundle bunlde) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (requestLastLocation != null && locationClient.isConnected()) {
|
||||
requestLastLocation.onLocationChanged(locationClient.getLastLocation());
|
||||
requestLastLocation = null;
|
||||
}
|
||||
if (requestLocationUpdates != null && locationClient.isConnected()) {
|
||||
LocationRequest locationRequest = new LocationRequest().setPriority(
|
||||
LocationRequest.PRIORITY_HIGH_ACCURACY).setInterval(requestLocationUpdatesTime)
|
||||
.setFastestInterval(requestLocationUpdatesTime)
|
||||
.setSmallestDisplacement(requestLocationUpdatesDistance);
|
||||
locationClient.requestLocationUpdates(
|
||||
locationRequest, requestLocationUpdates, handler.getLooper());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private final OnConnectionFailedListener
|
||||
onConnectionFailedListener = new OnConnectionFailedListener() {
|
||||
@Override
|
||||
public void onConnectionFailed(ConnectionResult connectionResult) {}
|
||||
};
|
||||
|
||||
private final Handler handler;
|
||||
private final LocationClient locationClient;
|
||||
private final LocationManager locationManager;
|
||||
private final ContentResolver contentResolver;
|
||||
private final GoogleSettingsObserver observer;
|
||||
|
||||
private boolean isAvailable;
|
||||
private boolean isAllowed;
|
||||
private LocationListener requestLastLocation;
|
||||
private LocationListener requestLocationUpdates;
|
||||
private float requestLocationUpdatesDistance;
|
||||
private long requestLocationUpdatesTime;
|
||||
|
||||
public MyTracksLocationManager(Context context, Looper looper, boolean enableLocaitonClient) {
|
||||
this.handler = new Handler(looper);
|
||||
|
||||
if (enableLocaitonClient) {
|
||||
locationClient = new LocationClient(context, connectionCallbacks, onConnectionFailedListener);
|
||||
locationClient.connect();
|
||||
} else {
|
||||
locationClient = null;
|
||||
}
|
||||
|
||||
locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
|
||||
contentResolver = context.getContentResolver();
|
||||
observer = new GoogleSettingsObserver(handler);
|
||||
|
||||
isAvailable = GoogleLocationUtils.isAvailable(context);
|
||||
isAllowed = isUseLocationForServicesOn();
|
||||
|
||||
contentResolver.registerContentObserver(
|
||||
Uri.parse(GOOGLE_SETTINGS_CONTENT_URI + "/" + USE_LOCATION_FOR_SERVICES), false, observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the {@link MyTracksLocationManager}.
|
||||
*/
|
||||
public void close() {
|
||||
if (locationClient != null) {
|
||||
locationClient.disconnect();
|
||||
}
|
||||
contentResolver.unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if allowed to access the location manager. Returns true if
|
||||
* there is no Google location settings or the Google location settings allows
|
||||
* access to location data.
|
||||
*/
|
||||
public boolean isAllowed() {
|
||||
return isAllowed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if gps provider is enabled.
|
||||
*/
|
||||
public boolean isGpsProviderEnabled() {
|
||||
if (!isAllowed) {
|
||||
return false;
|
||||
}
|
||||
String provider = LocationManager.GPS_PROVIDER;
|
||||
if (locationManager.getProvider(provider) == null) {
|
||||
return false;
|
||||
}
|
||||
return locationManager.isProviderEnabled(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request last location.
|
||||
*
|
||||
* @param locationListener location listener
|
||||
*/
|
||||
public void requestLastLocation(final LocationListener locationListener) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!isAllowed) {
|
||||
requestLastLocation = null;
|
||||
locationListener.onLocationChanged(null);
|
||||
} else {
|
||||
requestLastLocation = locationListener;
|
||||
connectionCallbacks.onConnected(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests location updates. This is an ongoing request, thus the caller
|
||||
* needs to check the status of {@link #isAllowed}.
|
||||
*
|
||||
* @param minTime the minimal time
|
||||
* @param minDistance the minimal distance
|
||||
* @param locationListener the location listener
|
||||
*/
|
||||
public void requestLocationUpdates(
|
||||
final long minTime, final float minDistance, final LocationListener locationListener) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestLocationUpdatesTime = minTime;
|
||||
requestLocationUpdatesDistance = minDistance;
|
||||
requestLocationUpdates = locationListener;
|
||||
connectionCallbacks.onConnected(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes location updates.
|
||||
*
|
||||
* @param locationListener the location listener
|
||||
*/
|
||||
public void removeLocationUpdates(final LocationListener locationListener) {
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
requestLocationUpdates = null;
|
||||
if (locationClient != null && locationClient.isConnected()) {
|
||||
locationClient.removeLocationUpdates(locationListener);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the Google location settings for
|
||||
* {@link #USE_LOCATION_FOR_SERVICES} is on.
|
||||
*/
|
||||
private boolean isUseLocationForServicesOn() {
|
||||
if (!isAvailable) {
|
||||
return true;
|
||||
}
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = contentResolver.query(Uri.parse(GOOGLE_SETTINGS_CONTENT_URI), new String[] { VALUE },
|
||||
NAME + "=?", new String[] { USE_LOCATION_FOR_SERVICES }, null);
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
return USE_LOCATION_FOR_SERVICES_ON.equals(cursor.getString(0));
|
||||
}
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to read " + USE_LOCATION_FOR_SERVICES, e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+167
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services;
|
||||
|
||||
import com.google.android.apps.mytracks.util.TrackRecordingServiceConnectionUtils;
|
||||
import com.google.android.maps.mytracks.BuildConfig;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.os.IBinder;
|
||||
import android.os.IBinder.DeathRecipient;
|
||||
import android.os.RemoteException;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Wrapper for the track recording service. This handles service
|
||||
* start/bind/unbind/stop. The service must be started before it can be bound.
|
||||
* Returns the service if it is started and bound.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackRecordingServiceConnection {
|
||||
|
||||
private static final String TAG = TrackRecordingServiceConnection.class.getSimpleName();
|
||||
|
||||
private final DeathRecipient deathRecipient = new DeathRecipient() {
|
||||
@Override
|
||||
public void binderDied() {
|
||||
Log.d(TAG, "Service died.");
|
||||
setTrackRecordingService(null);
|
||||
}
|
||||
};
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
Log.i(TAG, "Connected to the service.");
|
||||
try {
|
||||
service.linkToDeath(deathRecipient, 0);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to bind a death recipient.", e);
|
||||
}
|
||||
setTrackRecordingService(ITrackRecordingService.Stub.asInterface(service));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
Log.i(TAG, "Disconnected from the service.");
|
||||
setTrackRecordingService(null);
|
||||
}
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
private final Runnable callback;
|
||||
private ITrackRecordingService trackRecordingService;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param callback the callback to invoke when the service binding changes
|
||||
*/
|
||||
public TrackRecordingServiceConnection(Context context, Runnable callback) {
|
||||
this.context = context;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts and binds the service.
|
||||
*/
|
||||
public void startAndBind() {
|
||||
bindService(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the service if it is started.
|
||||
*/
|
||||
public void bindIfStarted() {
|
||||
bindService(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds and stops the service.
|
||||
*/
|
||||
public void unbindAndStop() {
|
||||
unbind();
|
||||
context.stopService(new Intent(context, TrackRecordingService.class));
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds the service (but leave it running).
|
||||
*/
|
||||
public void unbind() {
|
||||
try {
|
||||
context.unbindService(serviceConnection);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Means not bound to the service. OK to ignore.
|
||||
}
|
||||
setTrackRecordingService(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the track recording service if bound. Returns null otherwise
|
||||
*/
|
||||
public ITrackRecordingService getServiceIfBound() {
|
||||
if (trackRecordingService != null && !trackRecordingService.asBinder().isBinderAlive()) {
|
||||
setTrackRecordingService(null);
|
||||
return null;
|
||||
}
|
||||
return trackRecordingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the service if it is started.
|
||||
*
|
||||
* @param startIfNeeded start the service if needed
|
||||
*/
|
||||
private void bindService(boolean startIfNeeded) {
|
||||
if (trackRecordingService != null) {
|
||||
// Service is already started and bound.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startIfNeeded
|
||||
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
|
||||
Log.d(TAG, "Service is not started. Not binding it.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startIfNeeded) {
|
||||
Log.i(TAG, "Starting the service.");
|
||||
context.startService(new Intent(context, TrackRecordingService.class));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Binding the service.");
|
||||
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
|
||||
context.bindService(new Intent(context, TrackRecordingService.class), serviceConnection, flags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the trackRecordingService.
|
||||
*
|
||||
* @param value the value
|
||||
*/
|
||||
private void setTrackRecordingService(ITrackRecordingService value) {
|
||||
trackRecordingService = value;
|
||||
if (callback != null) {
|
||||
callback.run();
|
||||
}
|
||||
}
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorState;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothSocket;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Manages bluetooth connection. It has a thread for connecting with a bluetooth
|
||||
* device and a thread for performing data transmission when connected.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothConnectionManager {
|
||||
|
||||
// My Tracks UUID
|
||||
public static final UUID MY_TRACKS_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
|
||||
|
||||
// Message types sent to hander
|
||||
public static final int MESSAGE_DEVICE_NAME = 1;
|
||||
public static final int MESSAGE_READ = 2;
|
||||
|
||||
// Key for storing the device name
|
||||
public static final String KEY_DEVICE_NAME = "device_name";
|
||||
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
private final BluetoothAdapter bluetoothAdapter;
|
||||
private final Handler handler;
|
||||
private final MessageParser messageParser;
|
||||
private SensorState sensorState;
|
||||
|
||||
private ConnectThread connectThread;
|
||||
private ConnectedThread connectedThread;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param bluetoothAdapter the bluetooth adapter
|
||||
* @param handler a hander for sending messages back to the UI activity
|
||||
* @param messageParser a message parser
|
||||
*/
|
||||
public BluetoothConnectionManager(
|
||||
BluetoothAdapter bluetoothAdapter, Handler handler, MessageParser messageParser) {
|
||||
this.bluetoothAdapter = bluetoothAdapter;
|
||||
this.handler = handler;
|
||||
this.messageParser = messageParser;
|
||||
this.sensorState = SensorState.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sensor state.
|
||||
*/
|
||||
public synchronized SensorState getSensorState() {
|
||||
return sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sensor state.
|
||||
*
|
||||
* @param sensorState the sensor state
|
||||
*/
|
||||
private synchronized void setState(Sensor.SensorState sensorState) {
|
||||
this.sensorState = sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the bluetooth connection manager.
|
||||
*/
|
||||
public synchronized void reset() {
|
||||
cancelThreads();
|
||||
setState(Sensor.SensorState.NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels all the threads.
|
||||
*/
|
||||
private void cancelThreads() {
|
||||
if (connectThread != null) {
|
||||
connectThread.cancel();
|
||||
connectThread = null;
|
||||
}
|
||||
if (connectedThread != null) {
|
||||
connectedThread.cancel();
|
||||
connectedThread = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects to a bluetooth device.
|
||||
*
|
||||
* @param bluetoothDevice the bluetooth device
|
||||
*/
|
||||
public synchronized void connect(BluetoothDevice bluetoothDevice) {
|
||||
Log.d(TAG, "connect to: " + bluetoothDevice);
|
||||
cancelThreads();
|
||||
|
||||
connectThread = new ConnectThread(bluetoothDevice);
|
||||
connectThread.start();
|
||||
setState(Sensor.SensorState.CONNECTING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the ConnectedThread to read data.
|
||||
*
|
||||
* @param bluetoothSocket the bluetooth socket
|
||||
* @param bluetoothDevice the bluetooth device
|
||||
*/
|
||||
private synchronized void connected(
|
||||
BluetoothSocket bluetoothSocket, BluetoothDevice bluetoothDevice) {
|
||||
cancelThreads();
|
||||
|
||||
connectedThread = new ConnectedThread(bluetoothSocket);
|
||||
connectedThread.start();
|
||||
|
||||
// Send the device name to the handler
|
||||
Message message = handler.obtainMessage(MESSAGE_DEVICE_NAME);
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putString(KEY_DEVICE_NAME, bluetoothDevice.getName());
|
||||
message.setData(bundle);
|
||||
handler.sendMessage(message);
|
||||
|
||||
setState(Sensor.SensorState.CONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* A thread to connect to a bluetooth device.
|
||||
*/
|
||||
private class ConnectThread extends Thread {
|
||||
private final BluetoothSocket bluetoothSocket;
|
||||
private final BluetoothDevice bluetoothDevice;
|
||||
|
||||
public ConnectThread(BluetoothDevice device) {
|
||||
setName("ConnectThread-" + device.getName());
|
||||
this.bluetoothDevice = device;
|
||||
BluetoothSocket tmp = null;
|
||||
try {
|
||||
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to get blueooth socket.", e);
|
||||
}
|
||||
bluetoothSocket = tmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (bluetoothAdapter == null) {
|
||||
BluetoothConnectionManager.this.reset();
|
||||
return;
|
||||
}
|
||||
// Cancel discovery to prevent slow down
|
||||
bluetoothAdapter.cancelDiscovery();
|
||||
|
||||
try {
|
||||
bluetoothSocket.connect();
|
||||
} catch (IOException connectException) {
|
||||
Log.i(TAG, "Unable to connect.", connectException);
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
try {
|
||||
bluetoothSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close blueooth socket.", e);
|
||||
}
|
||||
// Reset the bluetooth connection manager
|
||||
BluetoothConnectionManager.this.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset the ConnectThread since we are done
|
||||
synchronized (BluetoothConnectionManager.this) {
|
||||
connectThread = null;
|
||||
}
|
||||
|
||||
// Start the connected thread
|
||||
connected(bluetoothSocket, bluetoothDevice);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels this thread.
|
||||
*/
|
||||
public void cancel() {
|
||||
try {
|
||||
bluetoothSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close bluetooth socket.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This thread handles data transmission when connected.
|
||||
*/
|
||||
private class ConnectedThread extends Thread {
|
||||
private final BluetoothSocket bluetoothSSocket;
|
||||
private final InputStream inputStream;
|
||||
|
||||
public ConnectedThread(BluetoothSocket bluetoothSocket) {
|
||||
this.bluetoothSSocket = bluetoothSocket;
|
||||
InputStream tmp = null;
|
||||
|
||||
try {
|
||||
tmp = bluetoothSocket.getInputStream();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to get input stream.", e);
|
||||
}
|
||||
inputStream = tmp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
byte[] buffer = new byte[messageParser.getFrameSize()];
|
||||
int bytes; // bytes read
|
||||
int offset = 0;
|
||||
|
||||
// Keep listening to the inputStream while connected
|
||||
while (true) {
|
||||
try {
|
||||
// Read from the inputStream
|
||||
bytes = inputStream.read(buffer, offset, messageParser.getFrameSize() - offset);
|
||||
|
||||
if (bytes == -1) { throw new IOException("EOF reached."); }
|
||||
|
||||
offset += bytes;
|
||||
|
||||
if (offset != messageParser.getFrameSize()) {
|
||||
// Partial frame received. Call read again to receive the rest.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!messageParser.isValid(buffer)) {
|
||||
int index = messageParser.findNextAlignment(buffer);
|
||||
if (index == -1) {
|
||||
Log.w(TAG, "Could not find any valid data. Drop data.");
|
||||
offset = 0;
|
||||
continue;
|
||||
}
|
||||
Log.w(TAG, "Misaligned data. Found new message at " + index + ". Recovering...");
|
||||
offset = messageParser.getFrameSize() - index;
|
||||
System.arraycopy(buffer, index, buffer, 0, offset);
|
||||
continue;
|
||||
}
|
||||
|
||||
offset = 0;
|
||||
|
||||
// Send a copy of the obtained bytes to the handler to avoid memory
|
||||
// inconsistency issues
|
||||
handler.obtainMessage(MESSAGE_READ, bytes, -1, buffer.clone()).sendToTarget();
|
||||
} catch (IOException e) {
|
||||
Log.i(TAG, "Bluetooth connection lost.", e);
|
||||
setState(Sensor.SensorState.DISCONNECTED);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels this thread.
|
||||
*/
|
||||
public void cancel() {
|
||||
try {
|
||||
bluetoothSSocket.close();
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to close bluetooth socket.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorState;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* Bluetooth sensor manager.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class BluetoothSensorManager extends SensorManager {
|
||||
|
||||
private static final BluetoothAdapter bluetoothAdapter = getDefaultBluetoothAdapter();
|
||||
private static final String TAG = BluetoothConnectionManager.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Gets the default bluetooth adapter.
|
||||
*/
|
||||
private static BluetoothAdapter getDefaultBluetoothAdapter() {
|
||||
// If from the main application thread, return directly
|
||||
if (Thread.currentThread().equals(Looper.getMainLooper().getThread())) {
|
||||
return BluetoothAdapter.getDefaultAdapter();
|
||||
}
|
||||
|
||||
// Get the default adapter from the main application thread
|
||||
final ArrayList<BluetoothAdapter> adapters = new ArrayList<BluetoothAdapter>(1);
|
||||
final Object mutex = new Object();
|
||||
|
||||
Handler handler = new Handler(Looper.getMainLooper());
|
||||
handler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
adapters.add(BluetoothAdapter.getDefaultAdapter());
|
||||
synchronized (mutex) {
|
||||
mutex.notify();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
while (adapters.isEmpty()) {
|
||||
synchronized (mutex) {
|
||||
try {
|
||||
mutex.wait(1000L);
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, "Interrupted while waiting for default bluetooth adapter", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (adapters.get(0) == null) {
|
||||
Log.w(TAG, "No bluetooth adapter found.");
|
||||
return null;
|
||||
}
|
||||
return adapters.get(0);
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
private final MessageParser messageParser;
|
||||
private final BluetoothConnectionManager bluetoothConnectionManager;
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
|
||||
// Handler that gets information back from the bluetoothConnectionManager
|
||||
private final Handler messageHandler = new Handler(Looper.getMainLooper()) {
|
||||
@Override
|
||||
public void handleMessage(Message message) {
|
||||
switch (message.what) {
|
||||
case BluetoothConnectionManager.MESSAGE_DEVICE_NAME:
|
||||
String deviceName = message.getData()
|
||||
.getString(BluetoothConnectionManager.KEY_DEVICE_NAME);
|
||||
Toast.makeText(context, context.getString(R.string.settings_sensor_connected, deviceName),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
break;
|
||||
case BluetoothConnectionManager.MESSAGE_READ:
|
||||
try {
|
||||
byte[] readBuf = (byte[]) message.obj;
|
||||
sensorDataSet = messageParser.parseBuffer(readBuf);
|
||||
Log.d(TAG, "MESSAGE_READ: " + sensorDataSet);
|
||||
} catch (IllegalArgumentException e) {
|
||||
sensorDataSet = null;
|
||||
Log.i(TAG, "Unexpected exception on read", e);
|
||||
} catch (RuntimeException e) {
|
||||
sensorDataSet = null;
|
||||
Log.i(TAG, "Unexpected exception on read.", e);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
* @param messageParser the message parser
|
||||
*/
|
||||
public BluetoothSensorManager(Context context, MessageParser messageParser) {
|
||||
this.context = context;
|
||||
this.messageParser = messageParser;
|
||||
bluetoothConnectionManager = new BluetoothConnectionManager(
|
||||
bluetoothAdapter, messageHandler, messageParser);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return bluetoothAdapter != null && bluetoothAdapter.isEnabled();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpChannel() {
|
||||
if (!isEnabled()) {
|
||||
Log.w(TAG, "Bluetooth not enabled.");
|
||||
return;
|
||||
}
|
||||
String address = PreferencesUtils.getString(
|
||||
context, R.string.bluetooth_sensor_key, PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT);
|
||||
if (PreferencesUtils.BLUETOOTH_SENSOR_DEFAULT.equals(address)) {
|
||||
Log.w(TAG, "No blueooth address.");
|
||||
return;
|
||||
}
|
||||
Log.w(TAG, "Connecting to bluetooth address: " + address);
|
||||
|
||||
BluetoothDevice device;
|
||||
try {
|
||||
device = bluetoothAdapter.getRemoteDevice(address);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.d(TAG, "Unable to get remote device for: " + address, e);
|
||||
return;
|
||||
}
|
||||
bluetoothConnectionManager.connect(device);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDownChannel() {
|
||||
bluetoothConnectionManager.reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorState getSensorState() {
|
||||
return bluetoothConnectionManager.getSensorState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* An interface for parsing a byte array to a SensorData object.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface MessageParser {
|
||||
|
||||
public int getFrameSize();
|
||||
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] readBuff);
|
||||
|
||||
public boolean isValid(byte[] buffer);
|
||||
|
||||
public int findNextAlignment(byte[] buffer);
|
||||
}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
|
||||
/**
|
||||
* An implementation of a Sensor MessageParser for Polar Wearlink Bluetooth HRM.
|
||||
*
|
||||
* Polar Bluetooth Wearlink packet example;
|
||||
* Hdr Len Chk Seq Status HeartRate RRInterval_16-bits
|
||||
* FE 08 F7 06 F1 48 03 64
|
||||
* where;
|
||||
* Hdr always = 254 (0xFE),
|
||||
* Chk = 255 - Len
|
||||
* Seq range 0 to 15
|
||||
* Status = Upper nibble may be battery voltage
|
||||
* bit 0 is Beat Detection flag.
|
||||
*
|
||||
* Additional packet examples;
|
||||
* FE 08 F7 06 F1 48 03 64
|
||||
* FE 0A F5 06 F1 48 03 64 03 70
|
||||
*
|
||||
* @author John R. Gerthoffer
|
||||
*/
|
||||
public class PolarMessageParser implements MessageParser {
|
||||
|
||||
private int lastHeartRate = 0;
|
||||
|
||||
/**
|
||||
* Applies Polar packet validation rules to buffer.
|
||||
* Polar packets are checked for following;
|
||||
* offset 0 = header byte, 254 (0xFE).
|
||||
* offset 1 = packet length byte, 8, 10, 12, 14.
|
||||
* offset 2 = check byte, 255 - packet length.
|
||||
* offset 3 = sequence byte, range from 0 to 15.
|
||||
*
|
||||
* @param buffer an array of bytes to parse
|
||||
* @param i buffer offset to beginning of packet.
|
||||
* @return whether buffer has a valid packet at offset i
|
||||
*/
|
||||
private boolean packetValid (byte[] buffer, int i) {
|
||||
boolean headerValid = (buffer[i] & 0xFF) == 0xFE;
|
||||
boolean checkbyteValid = (buffer[i + 2] & 0xFF) == (0xFF - (buffer[i + 1] & 0xFF));
|
||||
boolean sequenceValid = (buffer[i + 3] & 0xFF) < 16;
|
||||
|
||||
return headerValid && checkbyteValid && sequenceValid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
|
||||
|
||||
int heartRate = 0;
|
||||
boolean heartrateValid = false;
|
||||
|
||||
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
|
||||
for (int i = 0; i < buffer.length - 8; i++) {
|
||||
heartrateValid = packetValid(buffer,i);
|
||||
if (heartrateValid) {
|
||||
heartRate = buffer[i + 5] & 0xFF;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If our buffer is corrupted, use decaying last good value.
|
||||
if(!heartrateValid) {
|
||||
heartRate = (int) (lastHeartRate * 0.8);
|
||||
if(heartRate < 50)
|
||||
heartRate = 0;
|
||||
}
|
||||
|
||||
lastHeartRate = heartRate; // Remember good value for next time.
|
||||
|
||||
// Heart Rate
|
||||
Sensor.SensorData.Builder b = Sensor.SensorData.newBuilder()
|
||||
.setValue(heartRate)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
|
||||
Sensor.SensorDataSet sds = Sensor.SensorDataSet.newBuilder()
|
||||
.setCreationTime(System.currentTimeMillis())
|
||||
.setHeartRate(b)
|
||||
.build();
|
||||
|
||||
return sds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies packet validation rules to buffer
|
||||
*
|
||||
* @param buffer an array of bytes to parse
|
||||
* @return whether buffer has a valid packet starting at index zero
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(byte[] buffer) {
|
||||
return packetValid(buffer,0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Polar uses variable packet sizes; 8, 10, 12, 14 and rarely 16.
|
||||
* The most frequent are 8 and 10.
|
||||
* We will wait for 16 bytes.
|
||||
* This way, we are assured of getting one good one.
|
||||
*
|
||||
* @return the size of buffer needed to parse a good packet
|
||||
*/
|
||||
@Override
|
||||
public int getFrameSize() {
|
||||
return 16;
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches buffer for the beginning of a valid packet.
|
||||
*
|
||||
* @param buffer an array of bytes to parse
|
||||
* @return index to beginning of good packet, or -1 if none found.
|
||||
*/
|
||||
@Override
|
||||
public int findNextAlignment(byte[] buffer) {
|
||||
// Minimum length Polar packets is 8, so stop search 8 bytes before buffer ends.
|
||||
for (int i = 0; i < buffer.length - 8; i++) {
|
||||
if (packetValid(buffer,i)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A Polar heart rate sensor manager.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class PolarSensorManager extends BluetoothSensorManager {
|
||||
public PolarSensorManager(Context context) {
|
||||
super(context, new PolarMessageParser());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorState;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* Manage the connection to a sensor.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public abstract class SensorManager {
|
||||
|
||||
private static final String TAG = SensorManager.class.getSimpleName();
|
||||
private static final long MAX_SENSOR_DATE_SET_AGE = 5000;
|
||||
private static final long MAX_SENSOR_STATE_AGE = 20000;
|
||||
private static final int RETRY_PERIOD = 20000;
|
||||
|
||||
private SensorState sensorState = SensorState.NONE;
|
||||
private long sensorStateTimestamp = System.currentTimeMillis();
|
||||
|
||||
private TimerTask timerTask;
|
||||
private Timer timer;
|
||||
|
||||
/**
|
||||
* Returns true if the sensor is enabled.
|
||||
*/
|
||||
public abstract boolean isEnabled();
|
||||
|
||||
/**
|
||||
* Sets up the sensor channel.
|
||||
*/
|
||||
protected abstract void setUpChannel();
|
||||
|
||||
/**
|
||||
* Tears down the sensor channel.
|
||||
*/
|
||||
protected abstract void tearDownChannel();
|
||||
|
||||
/**
|
||||
* Gets the sensor data set.
|
||||
*/
|
||||
public abstract SensorDataSet getSensorDataSet();
|
||||
|
||||
/**
|
||||
* Starts the sensor.
|
||||
*/
|
||||
public void startSensor() {
|
||||
setUpChannel();
|
||||
timerTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
switch (getSensorState()) {
|
||||
case CONNECTING:
|
||||
if (System.currentTimeMillis() - sensorStateTimestamp > MAX_SENSOR_STATE_AGE) {
|
||||
Log.i(TAG, "Retry setUpChannel");
|
||||
setUpChannel();
|
||||
}
|
||||
break;
|
||||
case NONE:
|
||||
case DISCONNECTED:
|
||||
setUpChannel();
|
||||
break;
|
||||
default:
|
||||
// CONNECTED or SENDING
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
timer = new Timer(SensorManager.class.getSimpleName());
|
||||
timer.schedule(timerTask, RETRY_PERIOD, RETRY_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the sensor.
|
||||
*/
|
||||
public void stopSensor() {
|
||||
if (timerTask != null) {
|
||||
timerTask.cancel();
|
||||
timerTask = null;
|
||||
}
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
}
|
||||
tearDownChannel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sensor state.
|
||||
*
|
||||
* @param sensorState the sensor state
|
||||
*/
|
||||
public void setSensorState(SensorState sensorState) {
|
||||
sensorStateTimestamp = System.currentTimeMillis();
|
||||
this.sensorState = sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sensor state.
|
||||
*/
|
||||
public SensorState getSensorState() {
|
||||
return sensorState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the sensor data set is valid.
|
||||
*/
|
||||
public boolean isSensorDataSetValid() {
|
||||
SensorDataSet sensorDataSet = getSensorDataSet();
|
||||
if (sensorDataSet == null) {
|
||||
return false;
|
||||
}
|
||||
return (System.currentTimeMillis() - sensorDataSet.getCreationTime()) < MAX_SENSOR_DATE_SET_AGE;
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager;
|
||||
import com.google.android.apps.mytracks.util.AnalyticsUtils;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A factory of {@link SensorManager}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SensorManagerFactory {
|
||||
|
||||
private static SensorManager systemSensorManager = null;
|
||||
private static SensorManager tempSensorManager = null;
|
||||
|
||||
private SensorManagerFactory() {}
|
||||
|
||||
/**
|
||||
* Gets the system sensor manager.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public static SensorManager getSystemSensorManager(Context context) {
|
||||
releaseTempSensorManager();
|
||||
releaseSystemSensorManager();
|
||||
systemSensorManager = getSensorManager(context, true);
|
||||
if (systemSensorManager != null) {
|
||||
systemSensorManager.startSensor();
|
||||
}
|
||||
return systemSensorManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the system sensor manager.
|
||||
*/
|
||||
public static void releaseSystemSensorManager() {
|
||||
if (systemSensorManager != null) {
|
||||
systemSensorManager.stopSensor();
|
||||
}
|
||||
systemSensorManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the temp sensor manager.
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public static SensorManager getTempSensorManager(Context context) {
|
||||
releaseTempSensorManager();
|
||||
if (systemSensorManager != null) {
|
||||
return null;
|
||||
}
|
||||
tempSensorManager = getSensorManager(context, false);
|
||||
if (tempSensorManager != null) {
|
||||
tempSensorManager.startSensor();
|
||||
}
|
||||
return tempSensorManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases the temp sensor manager.
|
||||
*/
|
||||
public static void releaseTempSensorManager() {
|
||||
if (tempSensorManager != null) {
|
||||
tempSensorManager.stopSensor();
|
||||
}
|
||||
tempSensorManager = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sensor manager.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
private static SensorManager getSensorManager(Context context, boolean sendPageViews) {
|
||||
String sensorType = PreferencesUtils.getString(
|
||||
context, R.string.sensor_type_key, PreferencesUtils.SENSOR_TYPE_DEFAULT);
|
||||
|
||||
if (sensorType.equals(context.getString(R.string.sensor_type_value_ant))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.SENSOR_ANT);
|
||||
}
|
||||
return new AntSensorManager(context);
|
||||
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_zephyr))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.SENSOR_ZEPHYR);
|
||||
}
|
||||
return new ZephyrSensorManager(context);
|
||||
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_polar))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, AnalyticsUtils.SENSOR_POLAR);
|
||||
}
|
||||
return new PolarSensorManager(context);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A collection of methods for message parsers.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Nico Laum
|
||||
*/
|
||||
public class SensorUtils {
|
||||
|
||||
private SensorUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract one unsigned short from a big endian byte array.
|
||||
*
|
||||
* @param buffer the buffer to extract the short from
|
||||
* @param index the first byte to be interpreted as part of the short
|
||||
* @return The unsigned short at the given index in the buffer
|
||||
*/
|
||||
public static int unsignedShortToInt(byte[] buffer, int index) {
|
||||
int r = (buffer[index] & 0xFF) << 8;
|
||||
r |= buffer[index + 1] & 0xFF;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract one unsigned short from a little endian byte array.
|
||||
*
|
||||
* @param buffer the buffer to extract the short from
|
||||
* @param index the first byte to be interpreted as part of the short
|
||||
* @return The unsigned short at the given index in the buffer
|
||||
*/
|
||||
public static int unsignedShortToIntLittleEndian(byte[] buffer, int index) {
|
||||
int r = buffer[index] & 0xFF;
|
||||
r |= (buffer[index + 1] & 0xFF) << 8;
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns CRC8 (polynomial 0x8C) from byte array buffer[start] to
|
||||
* (excluding) buffer[start + length]
|
||||
*
|
||||
* @param buffer the byte array of data (payload)
|
||||
* @param start the position in the byte array where the payload begins
|
||||
* @param length the length
|
||||
* @return CRC8 value
|
||||
*/
|
||||
public static byte getCrc8(byte[] buffer, int start, int length) {
|
||||
byte crc = 0x0;
|
||||
|
||||
for (int i = start; i < (start + length); i++) {
|
||||
crc = crc8PushByte(crc, buffer[i]);
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a CRC8 value by using the next byte passed to this method
|
||||
*
|
||||
* @param crc int of crc value
|
||||
* @param add the next byte to add to the CRC8 calculation
|
||||
*/
|
||||
private static byte crc8PushByte(byte crc, byte add) {
|
||||
crc = (byte) (crc ^ add);
|
||||
|
||||
for (int i = 0; i < 8; i++) {
|
||||
if ((crc & 0x1) != 0x0) {
|
||||
// Using a 0xFF bit assures that 0-bits are introduced during the shift operation.
|
||||
// Otherwise, implicit casts to signed int could shift in 1-bits if the signed bit is 1.
|
||||
crc = (byte) (((crc & 0xFF) >> 1) ^ 0x8C);
|
||||
} else {
|
||||
crc = (byte) ((crc & 0xFF) >> 1);
|
||||
}
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
public static String getStateAsString(Sensor.SensorState state, Context c) {
|
||||
switch (state) {
|
||||
case NONE:
|
||||
return c.getString(R.string.value_none);
|
||||
case CONNECTING:
|
||||
return c.getString(R.string.sensor_state_connecting);
|
||||
case CONNECTED:
|
||||
return c.getString(R.string.sensor_state_connected);
|
||||
case DISCONNECTED:
|
||||
return c.getString(R.string.sensor_state_disconnected);
|
||||
case SENDING:
|
||||
return c.getString(R.string.sensor_state_sending);
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2011 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A history of Zephyr stride counter reading.
|
||||
* These can be used as an alternate method to calculate the correct cadence.
|
||||
* This is a work around for an issue with some HxM firmware.
|
||||
*
|
||||
* @author Dominik Rottsches
|
||||
*/
|
||||
public class StrideReadings {
|
||||
|
||||
// visible for testing
|
||||
protected static final int NUM_READINGS_FOR_AVERAGE = 10;
|
||||
protected static final int MIN_READINGS_FOR_AVERAGE = 5;
|
||||
|
||||
protected static final int CADENCE_NOT_AVAILABLE = -1;
|
||||
|
||||
// TODO: Check whether 1Hz assumption is okay for cadence calculation
|
||||
// otherwise add heart beat timestamp to this list and compute
|
||||
// cadence from these timestamps.
|
||||
private final List<Integer> strideReadingsHistory;
|
||||
|
||||
public StrideReadings() {
|
||||
strideReadingsHistory = new LinkedList<Integer>();
|
||||
}
|
||||
|
||||
public void updateStrideReading(int numStrides) {
|
||||
// HRM/HxM documentation says, transmission frequency is 1 Hz,
|
||||
// let's keep last NUM_READINGS_FOR_AVERAGE readings.
|
||||
// TODO: Calibrate this using a reliable footpod / cadence sensor,
|
||||
// otherwise use heartbeat timestamp for calculation.
|
||||
strideReadingsHistory.add(0, numStrides);
|
||||
while (strideReadingsHistory.size() > NUM_READINGS_FOR_AVERAGE) {
|
||||
strideReadingsHistory.remove(strideReadingsHistory.size()-1);
|
||||
}
|
||||
}
|
||||
|
||||
public int getCadence() {
|
||||
if (strideReadingsHistory.size() < MIN_READINGS_FOR_AVERAGE) {
|
||||
// Bail out if we cannot really get a meaningful average yet.
|
||||
return CADENCE_NOT_AVAILABLE;
|
||||
}
|
||||
// Compute assuming 1 stride reading/second.
|
||||
int timeSinceOldestReadingSecs = strideReadingsHistory.size() - 1;
|
||||
int stridesThen = strideReadingsHistory.get(strideReadingsHistory.size()-1);
|
||||
int stridesNow = strideReadingsHistory.get(0);
|
||||
// Contrary to documentation stride value seems to roll over at 128.
|
||||
return Math.round( (float)(mod((stridesNow - stridesThen), 128)) /
|
||||
timeSinceOldestReadingSecs * 60);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modulo operation with positive return values, Java's remainder operator doesn't change sign.
|
||||
*
|
||||
* @return x mod y
|
||||
*/
|
||||
private static int mod(int x, int y)
|
||||
{
|
||||
int result = x % y;
|
||||
return result < 0 ? result + y : result;
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* An implementation of a Sensor MessageParser for Zephyr.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
* @author Dominik Rttsches
|
||||
*/
|
||||
public class ZephyrMessageParser implements MessageParser {
|
||||
|
||||
public static final int ZEPHYR_HXM_BYTE_STX = 0;
|
||||
public static final int ZEPHYR_HXM_BYTE_CRC = 58;
|
||||
public static final int ZEPHYR_HXM_BYTE_ETX = 59;
|
||||
|
||||
private static final byte[] CADENCE_BUG_FW_ID = {0x1A, 0x00, 0x31, 0x65, 0x50, 0x00, 0x31, 0x62};
|
||||
|
||||
private StrideReadings strideReadings;
|
||||
|
||||
@Override
|
||||
public Sensor.SensorDataSet parseBuffer(byte[] buffer) {
|
||||
Sensor.SensorDataSet.Builder sds =
|
||||
Sensor.SensorDataSet.newBuilder()
|
||||
.setCreationTime(System.currentTimeMillis());
|
||||
|
||||
Sensor.SensorData.Builder heartrate = Sensor.SensorData.newBuilder()
|
||||
.setValue(buffer[12] & 0xFF)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
sds.setHeartRate(heartrate);
|
||||
|
||||
Sensor.SensorData.Builder batteryLevel = Sensor.SensorData.newBuilder()
|
||||
.setValue(buffer[11])
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
sds.setBatteryLevel(batteryLevel);
|
||||
|
||||
setCadence(sds, buffer);
|
||||
|
||||
return sds.build();
|
||||
}
|
||||
|
||||
private void setCadence(Sensor.SensorDataSet.Builder sds, byte[] buffer) {
|
||||
// Device Firmware ID, Firmware Version, Hardware ID, Hardware Version
|
||||
// 0x1A00316550003162 produces erroneous values for Cadence and needs
|
||||
// a workaround based on the stride counter.
|
||||
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
|
||||
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
|
||||
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
|
||||
|
||||
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
|
||||
if (strideReadings == null) {
|
||||
strideReadings = new StrideReadings();
|
||||
}
|
||||
strideReadings.updateStrideReading(buffer[54] & 0xFF);
|
||||
|
||||
if (strideReadings.getCadence() != StrideReadings.CADENCE_NOT_AVAILABLE) {
|
||||
cadence.setValue(strideReadings.getCadence()).setState(Sensor.SensorState.SENDING);
|
||||
}
|
||||
} else {
|
||||
cadence
|
||||
.setValue(SensorUtils.unsignedShortToIntLittleEndian(buffer, 56) / 16)
|
||||
.setState(Sensor.SensorState.SENDING);
|
||||
}
|
||||
sds.setCadence(cadence);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(byte[] buffer) {
|
||||
// Check STX (Start of Text), ETX (End of Text) and CRC Checksum
|
||||
return buffer.length > ZEPHYR_HXM_BYTE_ETX
|
||||
&& buffer[ZEPHYR_HXM_BYTE_STX] == 0x02
|
||||
&& buffer[ZEPHYR_HXM_BYTE_ETX] == 0x03
|
||||
&& SensorUtils.getCrc8(buffer, 3, 55) == buffer[ZEPHYR_HXM_BYTE_CRC];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getFrameSize() {
|
||||
return 60;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int findNextAlignment(byte[] buffer) {
|
||||
// TODO test or understand this code.
|
||||
for (int i = 0; i < buffer.length - 1; i++) {
|
||||
if (buffer[i] == 0x03 && buffer[i+1] == 0x02) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A Zephyr heart rate sensor manager.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class ZephyrSensorManager extends BluetoothSensorManager {
|
||||
|
||||
public ZephyrSensorManager(Context context) {
|
||||
super(context, new ZephyrMessageParser());
|
||||
}
|
||||
}
|
||||
+586
@@ -0,0 +1,586 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.dsi.ant.AntDefine;
|
||||
import com.dsi.ant.AntInterface;
|
||||
import com.dsi.ant.AntInterfaceIntent;
|
||||
import com.dsi.ant.AntMesg;
|
||||
import com.dsi.ant.exception.AntInterfaceException;
|
||||
import com.dsi.ant.exception.AntServiceNotConnectedException;
|
||||
import com.google.android.apps.mytracks.content.Sensor;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorDataSet;
|
||||
import com.google.android.apps.mytracks.content.Sensor.SensorState;
|
||||
import com.google.android.apps.mytracks.services.sensors.SensorManager;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
/**
|
||||
* Ant Sensor Manager.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class AntSensorManager extends SensorManager {
|
||||
|
||||
/**
|
||||
* Channel States
|
||||
*/
|
||||
public enum ChannelStates {
|
||||
CLOSED, // Channel is closed
|
||||
PENDING_OPEN, // User has requested opening the channel, waiting for a reset
|
||||
SEARCHING, // Channel is opened, but has not received data
|
||||
TRACKING_STATUS, // Channel is opened and has received status data recently
|
||||
TRACKING_DATA, // Channel is opened and has received measurement data
|
||||
// recently
|
||||
OFFLINE // Channel is closed as the result of a search timeout
|
||||
}
|
||||
public static final short WILDCARD = 0;
|
||||
|
||||
private static final String TAG = AntSensorManager.class.getSimpleName();
|
||||
private static final int CHANNELS = 4;
|
||||
private static final String RADIO_ANT = "ant";
|
||||
private static final byte ANT_NETWORK = (byte) 0x01;
|
||||
|
||||
private final Context context;
|
||||
private final ChannelConfiguration channelConfig[];
|
||||
private final IntentFilter statusIntentFilter;
|
||||
private final AntInterface antInterface;
|
||||
|
||||
private boolean serviceConnected = false;
|
||||
private boolean hasClaimedInterface = false;
|
||||
|
||||
private SensorDataSet sensorDataSet = null;
|
||||
private long lastSensorDataSetTime = 0;
|
||||
private AntSensorValue antSensorValue = new AntSensorValue();
|
||||
|
||||
private boolean requestedReset = false;
|
||||
|
||||
private AntInterface.ServiceListener serviceListener = new AntInterface.ServiceListener() {
|
||||
@Override
|
||||
public void onServiceConnected() {
|
||||
setSensorState(Sensor.SensorState.CONNECTING);
|
||||
serviceConnected = true;
|
||||
try {
|
||||
hasClaimedInterface = antInterface.hasClaimedInterface();
|
||||
if (!hasClaimedInterface) {
|
||||
hasClaimedInterface = antInterface.claimInterface();
|
||||
}
|
||||
if (!hasClaimedInterface) {
|
||||
tryClaimAnt();
|
||||
return;
|
||||
}
|
||||
if (!antInterface.isEnabled()) {
|
||||
antInterface.enable();
|
||||
}
|
||||
requestReset();
|
||||
enableDataMessage(true);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected() {
|
||||
serviceConnected = false;
|
||||
if (hasClaimedInterface) {
|
||||
enableDataMessage(false);
|
||||
}
|
||||
setSensorState(SensorState.DISCONNECTED);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the context
|
||||
*/
|
||||
public AntSensorManager(Context context) {
|
||||
this.context = context;
|
||||
|
||||
channelConfig = new ChannelConfiguration[CHANNELS];
|
||||
channelConfig[0] = new HeartRateChannelConfiguration();
|
||||
channelConfig[1] = new SpeedDistanceChannelConfiguration();
|
||||
channelConfig[2] = new BikeCadenceChannelConfiguration();
|
||||
channelConfig[3] = new CombinedBikeChannelConfiguration();
|
||||
|
||||
statusIntentFilter = new IntentFilter();
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLED_ACTION);
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_ENABLING_ACTION);
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_DISABLED_ACTION);
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_DISABLING_ACTION);
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_RESET_ACTION);
|
||||
statusIntentFilter.addAction(AntInterfaceIntent.ANT_INTERFACE_CLAIMED_ACTION);
|
||||
statusIntentFilter.addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED);
|
||||
|
||||
antInterface = new AntInterface();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized void setUpChannel() {
|
||||
tearDownChannel();
|
||||
if (AntInterface.hasAntSupport(context)) {
|
||||
context.registerReceiver(statusReceiver, statusIntentFilter);
|
||||
if (!antInterface.initService(context, serviceListener)) {
|
||||
AntInterface.goToMarket(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void tearDownChannel() {
|
||||
try {
|
||||
context.unregisterReceiver(statusReceiver);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Can safely ignore
|
||||
}
|
||||
enableDataMessage(false);
|
||||
if (serviceConnected) {
|
||||
try {
|
||||
if (hasClaimedInterface) {
|
||||
antInterface.releaseInterface();
|
||||
}
|
||||
hasClaimedInterface = false;
|
||||
antInterface.stopRequestForceClaimInterface();
|
||||
} catch (AntServiceNotConnectedException e) {
|
||||
// Can safely ignore
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.w(TAG, "Exception in tearDonwChannel.", e);
|
||||
}
|
||||
antInterface.releaseService();
|
||||
serviceConnected = false;
|
||||
}
|
||||
setSensorState(SensorState.DISCONNECTED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SensorDataSet getSensorDataSet() {
|
||||
return sensorDataSet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to claim the ant interface.
|
||||
*/
|
||||
private void tryClaimAnt() {
|
||||
try {
|
||||
antInterface.requestForceClaimInterface(context.getString(R.string.my_tracks_app_name));
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures ant radio.
|
||||
*/
|
||||
private void configureAntRadio() {
|
||||
try {
|
||||
if (serviceConnected && hasClaimedInterface && antInterface.isEnabled()) {
|
||||
try {
|
||||
antInterface.ANTDisableEventBuffering();
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.e(TAG, "Cannot disable event buffering.", e);
|
||||
}
|
||||
} else {
|
||||
Log.i(TAG, "Cannot disable event buffering now.");
|
||||
}
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.e(TAG, "Unable to check enabled state.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles ant error.
|
||||
*/
|
||||
private void handleAntError() {
|
||||
clearAllChannels();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a channel.
|
||||
*
|
||||
* @param channel the channel
|
||||
*/
|
||||
private void openChannel(byte channel) {
|
||||
short deviceNumber = (short) PreferencesUtils.getInt(
|
||||
context, channelConfig[channel].getDeviceIdKey(), WILDCARD);
|
||||
channelConfig[channel].setDeviceNumber(deviceNumber);
|
||||
channelConfig[channel].setChannelState(ChannelStates.PENDING_OPEN);
|
||||
setupAntChannel(ANT_NETWORK, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a channel.
|
||||
*
|
||||
* @param channel the channel
|
||||
*/
|
||||
private void closeChannel(byte channel) {
|
||||
channelConfig[channel].setInitializing(false);
|
||||
channelConfig[channel].setDeinitializing(true);
|
||||
channelConfig[channel].setChannelState(ChannelStates.CLOSED);
|
||||
try {
|
||||
antInterface.ANTCloseChannel(channel);
|
||||
// Note, unassign channel after getting channel closed event
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.w(TAG, "Unable to close channel: " + channel, e);
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all channels.
|
||||
*/
|
||||
private void clearAllChannels() {
|
||||
for (int i = 0; i < CHANNELS; i++) {
|
||||
channelConfig[i].setChannelState(ChannelStates.CLOSED);
|
||||
}
|
||||
setSensorState(SensorState.DISCONNECTED);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests reset.
|
||||
*/
|
||||
private void requestReset() {
|
||||
try {
|
||||
requestedReset = true;
|
||||
antInterface.ANTResetSystem();
|
||||
configureAntRadio();
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.e(TAG, "Unable to reset ant.", e);
|
||||
requestedReset = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
if (antInterface == null || !antInterface.isServiceConnected()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return antInterface.isEnabled();
|
||||
} catch (AntInterfaceException e) {
|
||||
Log.w(TAG, "Unable to check enabled.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private final BroadcastReceiver statusReceiver = new BroadcastReceiver() {
|
||||
public void onReceive(Context c, Intent intent) {
|
||||
String ANTAction = intent.getAction();
|
||||
if (ANTAction.equals(AntInterfaceIntent.ANT_DISABLED_ACTION)) {
|
||||
clearAllChannels();
|
||||
} else if (ANTAction.equals(AntInterfaceIntent.ANT_RESET_ACTION)) {
|
||||
if (!requestedReset) {
|
||||
// Someone else triggered an ant reset
|
||||
clearAllChannels();
|
||||
} else {
|
||||
requestedReset = false;
|
||||
configureAntRadio();
|
||||
}
|
||||
} else if (ANTAction.equals(AntInterfaceIntent.ANT_INTERFACE_CLAIMED_ACTION)) {
|
||||
boolean wasClaimed = hasClaimedInterface;
|
||||
|
||||
// Could also read ANT_INTERFACE_CLAIMED_PID from intent and see if it
|
||||
// matches the current process PID.
|
||||
try {
|
||||
hasClaimedInterface = antInterface.hasClaimedInterface();
|
||||
if (hasClaimedInterface) {
|
||||
enableDataMessage(true);
|
||||
} else {
|
||||
if (wasClaimed) {
|
||||
// Claimed by another application
|
||||
enableDataMessage(false);
|
||||
setSensorState(SensorState.DISCONNECTED);
|
||||
}
|
||||
}
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
} else if (ANTAction.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
|
||||
if (isAirPlaneMode()) {
|
||||
clearAllChannels();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver dataReceiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context c, Intent intent) {
|
||||
if (intent.getAction().equals(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION)) {
|
||||
byte[] antRxMessage = intent.getByteArrayExtra(AntInterfaceIntent.ANT_MESSAGE);
|
||||
byte channel;
|
||||
switch (antRxMessage[AntMesg.MESG_ID_OFFSET]) {
|
||||
case AntMesg.MESG_BROADCAST_DATA_ID:
|
||||
case AntMesg.MESG_ACKNOWLEDGED_DATA_ID:
|
||||
channel = antRxMessage[AntMesg.MESG_DATA_OFFSET];
|
||||
if (channelConfig[channel].getChannelState() != ChannelStates.CLOSED) {
|
||||
channelConfig[channel].setChannelState(ChannelStates.TRACKING_DATA);
|
||||
}
|
||||
|
||||
if (channelConfig[channel].getDeviceNumber() == WILDCARD) {
|
||||
try {
|
||||
antInterface.ANTRequestMessage(channel, AntMesg.MESG_CHANNEL_ID_ID);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
channelConfig[channel].decodeMessage(antRxMessage, antSensorValue);
|
||||
setSensorDataSet();
|
||||
break;
|
||||
case AntMesg.MESG_RESPONSE_EVENT_ID:
|
||||
handleResponseEventMessage(antRxMessage);
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_ID_ID:
|
||||
channel = antRxMessage[AntMesg.MESG_DATA_OFFSET];
|
||||
short deviceNumber = (short) ((antRxMessage[AntMesg.MESG_DATA_OFFSET + 1] & 0xFF
|
||||
| ((antRxMessage[AntMesg.MESG_DATA_OFFSET + 2] & 0xFF) << 8)) & 0xFFFF);
|
||||
channelConfig[channel].setDeviceNumber(deviceNumber);
|
||||
PreferencesUtils.setInt(context, channelConfig[channel].getDeviceIdKey(), deviceNumber);
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.settings_sensor_connected, deviceNumber),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
setSensorState(SensorState.CONNECTED);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles response event message.
|
||||
*
|
||||
* @param message the message
|
||||
*/
|
||||
private void handleResponseEventMessage(byte[] message) {
|
||||
// For a list of possible message codes see ANT Message Protocol and Usage
|
||||
// section 9.5.6.1 available from thisisant.com
|
||||
byte channel = message[AntMesg.MESG_DATA_OFFSET];
|
||||
if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_EVENT_ID)
|
||||
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.EVENT_RX_SEARCH_TIMEOUT)) {
|
||||
// A channel timed out searching, unassign it
|
||||
channelConfig[channel].setInitializing(false);
|
||||
channelConfig[channel].setDeinitializing(false);
|
||||
channelConfig[channel].setChannelState(ChannelStates.OFFLINE);
|
||||
try {
|
||||
antInterface.ANTUnassignChannel(channel);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
setSensorState(SensorState.DISCONNECTED);
|
||||
}
|
||||
|
||||
if (channelConfig[channel].isInitializing()) {
|
||||
if (message[AntMesg.MESG_DATA_OFFSET + 2] != 0) {
|
||||
// Error response
|
||||
Log.e(TAG, String.format("Error code(%#02x) on message ID(%#02x) on channel %d",
|
||||
message[AntMesg.MESG_DATA_OFFSET + 2], message[AntMesg.MESG_DATA_OFFSET + 1],
|
||||
channel));
|
||||
} else {
|
||||
// Switch on message id
|
||||
switch (message[AntMesg.MESG_DATA_OFFSET + 1]) {
|
||||
case AntMesg.MESG_ASSIGN_CHANNEL_ID:
|
||||
try {
|
||||
antInterface.ANTSetChannelId(channel, channelConfig[channel].getDeviceNumber(),
|
||||
channelConfig[channel].getDeviceType(), ChannelConfiguration.TRANSMISSION_TYPE);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_ID_ID:
|
||||
try {
|
||||
antInterface.ANTSetChannelPeriod(channel, channelConfig[channel].getMessagPeriod());
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_MESG_PERIOD_ID:
|
||||
try {
|
||||
antInterface.ANTSetChannelRFFreq(channel, ChannelConfiguration.FREQUENCY);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_RADIO_FREQ_ID:
|
||||
try {
|
||||
// Disable high priority search
|
||||
antInterface.ANTSetChannelSearchTimeout(channel, (byte) 0);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_CHANNEL_SEARCH_TIMEOUT_ID:
|
||||
try {
|
||||
// Set search timeout to 30 seconds (low priority search)
|
||||
antInterface.ANTSetLowPriorityChannelSearchTimeout(channel, (byte) 12);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_SET_LP_SEARCH_TIMEOUT_ID:
|
||||
if (channelConfig[channel].getDeviceNumber() == WILDCARD) {
|
||||
try {
|
||||
// Configure proximity search, if using wild card search
|
||||
antInterface.ANTSetProximitySearch(
|
||||
channel, ChannelConfiguration.PROXIMITY_SEARCH);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
antInterface.ANTOpenChannel(channel);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_PROX_SEARCH_CONFIG_ID:
|
||||
try {
|
||||
antInterface.ANTOpenChannel(channel);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
break;
|
||||
case AntMesg.MESG_OPEN_CHANNEL_ID:
|
||||
channelConfig[channel].setInitializing(false);
|
||||
channelConfig[channel].setChannelState(ChannelStates.SEARCHING);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (channelConfig[channel].isDeinitializing()) {
|
||||
if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_EVENT_ID)
|
||||
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.EVENT_CHANNEL_CLOSED)) {
|
||||
try {
|
||||
antInterface.ANTUnassignChannel(channel);
|
||||
} catch (AntInterfaceException e) {
|
||||
handleAntError();
|
||||
}
|
||||
} else if ((message[AntMesg.MESG_DATA_OFFSET + 1] == AntMesg.MESG_UNASSIGN_CHANNEL_ID)
|
||||
&& (message[AntMesg.MESG_DATA_OFFSET + 2] == AntDefine.RESPONSE_NO_ERROR)) {
|
||||
channelConfig[channel].setDeinitializing(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets sensor data set.
|
||||
*/
|
||||
private void setSensorDataSet() {
|
||||
long now = System.currentTimeMillis();
|
||||
// Data comes in at ~4Hz rate from the sensors, so after >300 msec fresh
|
||||
// data is here from all the connected sensors
|
||||
if (now < lastSensorDataSetTime + 300) {
|
||||
return;
|
||||
}
|
||||
lastSensorDataSetTime = now;
|
||||
|
||||
SensorDataSet.Builder builder = Sensor.SensorDataSet.newBuilder();
|
||||
int heartRate = antSensorValue.getHeartRate();
|
||||
if (heartRate != -1) {
|
||||
builder.setHeartRate(Sensor.SensorData.newBuilder()
|
||||
.setValue(heartRate).setState(Sensor.SensorState.SENDING));
|
||||
}
|
||||
int cadence = antSensorValue.getCadence();
|
||||
if (cadence != -1) {
|
||||
builder.setCadence(Sensor.SensorData.newBuilder()
|
||||
.setValue(cadence).setState(Sensor.SensorState.SENDING));
|
||||
}
|
||||
sensorDataSet = builder.setCreationTime(now).build();
|
||||
setSensorState(SensorState.SENDING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up ant channel.
|
||||
*
|
||||
* @param networkNumber the network number
|
||||
* @param channel the channel
|
||||
*/
|
||||
private void setupAntChannel(byte networkNumber, byte channel) {
|
||||
try {
|
||||
channelConfig[channel].setInitializing(true);
|
||||
channelConfig[channel].setDeinitializing(false);
|
||||
|
||||
// Assign as slave channel on selected network
|
||||
antInterface.ANTAssignChannel(channel, AntDefine.PARAMETER_RX_NOT_TX, networkNumber);
|
||||
|
||||
// The rest of the channel configuration will occur after the response is
|
||||
// received in handleResponseEventMessage
|
||||
} catch (AntInterfaceException aie) {
|
||||
handleAntError();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables data message.
|
||||
*
|
||||
* @param enabled true to enable
|
||||
*/
|
||||
private void enableDataMessage(boolean enabled) {
|
||||
if (enabled) {
|
||||
context.registerReceiver(
|
||||
dataReceiver, new IntentFilter(AntInterfaceIntent.ANT_RX_MESSAGE_ACTION));
|
||||
for (int i = 0; i < CHANNELS; i++) {
|
||||
openChannel((byte) i);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
context.unregisterReceiver(dataReceiver);
|
||||
for (int i = 0; i < CHANNELS; i++) {
|
||||
closeChannel((byte) i);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Can safely ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if in airplane mode.
|
||||
*/
|
||||
private boolean isAirPlaneMode() {
|
||||
if (!Settings.System.getString(
|
||||
context.getContentResolver(), Settings.System.AIRPLANE_MODE_RADIOS)
|
||||
.contains(RADIO_ANT)) {
|
||||
return false;
|
||||
}
|
||||
if (Settings.System.getInt(context.getContentResolver(), Settings.System.AIRPLANE_MODE_ON, 0)
|
||||
== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Field field = Settings.System.class.getField("AIRPLANE_MODE_TOGGLEABLE_RADIOS");
|
||||
return !Settings.System.getString(context.getContentResolver(), (String) field.get(null))
|
||||
.contains(RADIO_ANT);
|
||||
} catch (Exception e) {
|
||||
// This is expected if the list does not yet exist, so just return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
/**
|
||||
* Contains the latest ant sensor value.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class AntSensorValue {
|
||||
|
||||
private int heartRate = -1;
|
||||
private int cadence = -1;
|
||||
|
||||
/**
|
||||
* Gets the heart rate.
|
||||
*/
|
||||
public int getHeartRate() {
|
||||
return heartRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the heart rate.
|
||||
*
|
||||
* @param heartRate the heart rate to set
|
||||
*/
|
||||
public void setHeartRate(int heartRate) {
|
||||
this.heartRate = heartRate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cadence.
|
||||
*/
|
||||
public int getCadence() {
|
||||
return cadence;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the cadence.
|
||||
*
|
||||
* @param cadence the cadence to set
|
||||
*/
|
||||
public void setCadence(int cadence) {
|
||||
this.cadence = cadence;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
/**
|
||||
* Bike cadence sensor channel configuration.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BikeCadenceChannelConfiguration extends ChannelConfiguration {
|
||||
|
||||
private static final int DEVICE_ID_KEY = R.string.ant_bike_cadence_sensor_id_key;
|
||||
private static final byte DEVICE_TYPE = 0x7A;
|
||||
private static final short MESSAGE_PERIOD = 8102;
|
||||
|
||||
private CadenceCounter cadenceCounter = new CadenceCounter();
|
||||
|
||||
@Override
|
||||
public int getDeviceIdKey() {
|
||||
return DEVICE_ID_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getDeviceType() {
|
||||
return DEVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getMessagPeriod() {
|
||||
return MESSAGE_PERIOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
|
||||
int revolutionCount = ((int) message[9] & 0xFF) + ((int) message[10] & 0xFF) * 256;
|
||||
int eventTime = ((int) message[7] & 0xFF) + ((int) message[8] & 0xFF) * 256;
|
||||
antSensorValue.setCadence(cadenceCounter.getEventsPerMinute(revolutionCount, eventTime));
|
||||
}
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import java.util.LinkedList;
|
||||
|
||||
/**
|
||||
* A counter that processes an Ant+ sensor data (count + event time) and returns
|
||||
* the instantaneous cadence value.
|
||||
*
|
||||
* @author Laszlo Molnar
|
||||
*/
|
||||
public class CadenceCounter {
|
||||
|
||||
private static final int MILLIS_PER_MINUTE = 60000;
|
||||
private static final int MAX_HISTORY_TIME_IN_MILLIS = 5000; // 5 seconds
|
||||
private static final int MAX_HISTORY_SIZE = 100;
|
||||
private static final int EVENT_TIME_PER_MINUTE = 60 * 1024;
|
||||
|
||||
/**
|
||||
* Cadence data.
|
||||
*
|
||||
* @author Laszlo Molnar
|
||||
*/
|
||||
private static class CadenceData {
|
||||
final long systemTime;
|
||||
final int count;
|
||||
final int eventTime;
|
||||
|
||||
private CadenceData(long systemTime, int count, int eventTime) {
|
||||
this.systemTime = systemTime;
|
||||
this.count = count;
|
||||
this.eventTime = eventTime;
|
||||
}
|
||||
}
|
||||
|
||||
// The last count
|
||||
private int lastCount;
|
||||
|
||||
// The last calculated cadence
|
||||
private int eventsPerMinute;
|
||||
|
||||
// The history of the previous sensor data, oldest first
|
||||
private LinkedList<CadenceData> history;
|
||||
|
||||
public CadenceCounter() {
|
||||
lastCount = -1;
|
||||
eventsPerMinute = 0;
|
||||
history = new LinkedList<CadenceData>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cadence value.
|
||||
*
|
||||
* @param count count
|
||||
* @param eventTime event time
|
||||
*/
|
||||
public int getEventsPerMinute(int count, int eventTime) {
|
||||
long now = System.currentTimeMillis();
|
||||
int countChange = (count - lastCount) & 0xFFFF;
|
||||
|
||||
if (lastCount < 0) {
|
||||
/*
|
||||
* eventTime for the initial count value is probably out of date, so not
|
||||
* updating the history.
|
||||
*/
|
||||
lastCount = count;
|
||||
eventsPerMinute = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
lastCount = count;
|
||||
|
||||
if (countChange != 0) {
|
||||
if (removeOldHistory(now)) {
|
||||
CadenceData lastCadenceData = history.getLast();
|
||||
int eventTimeChange = (eventTime - lastCadenceData.eventTime) & 0xFFFF;
|
||||
if (eventTimeChange != 0) {
|
||||
countChange = (count - lastCadenceData.count) & 0xFFFF;
|
||||
eventsPerMinute = countChange * EVENT_TIME_PER_MINUTE / eventTimeChange;
|
||||
}
|
||||
}
|
||||
history.addLast(new CadenceData(now, count, eventTime));
|
||||
return eventsPerMinute;
|
||||
} else {
|
||||
// The sensor has resent old data
|
||||
if (history.isEmpty()) {
|
||||
eventsPerMinute = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
CadenceData lastCadenceData = history.getLast();
|
||||
if ((now - lastCadenceData.systemTime) * eventsPerMinute < MILLIS_PER_MINUTE) {
|
||||
// The last eventsPerMinute is still valid
|
||||
return eventsPerMinute;
|
||||
}
|
||||
|
||||
// Update the history
|
||||
if (!removeOldHistory(now)) {
|
||||
eventsPerMinute = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Too much time has passed since the last count change. A smaller value
|
||||
* than eventsPerMinute must be returned. The value is calculated from the
|
||||
* history.
|
||||
*/
|
||||
return getValueFromHistory(now);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the cadence value from the history when the sensor only resends the
|
||||
* old data.
|
||||
*
|
||||
* @param now the current system time
|
||||
*/
|
||||
private int getValueFromHistory(long now) {
|
||||
CadenceData firstCadenceData = history.getFirst();
|
||||
CadenceData lastCadenceData = history.getLast();
|
||||
int eventTimeChange = (lastCadenceData.eventTime - firstCadenceData.eventTime) & 0xFFFF;
|
||||
int countChange = (lastCount - firstCadenceData.count) & 0xFFFF;
|
||||
|
||||
// (now - lastCadenceData) + (lastCadenceData - firstCadenceData)
|
||||
int systemTimeChange = (int) (now - lastCadenceData.systemTime
|
||||
+ (eventTimeChange * MILLIS_PER_MINUTE) / EVENT_TIME_PER_MINUTE);
|
||||
|
||||
/*
|
||||
* eventsPerMinute is not updated because it is still needed when a new
|
||||
* sensor event arrives.
|
||||
*/
|
||||
if (systemTimeChange == 0) {
|
||||
return eventsPerMinute;
|
||||
}
|
||||
int value = (countChange * MILLIS_PER_MINUTE) / systemTimeChange;
|
||||
|
||||
/*
|
||||
* Do not return a larger number than eventsPerMinute because this function
|
||||
* is only called when more time has passed after the last count change than
|
||||
* the interval from eventsPerMinute.
|
||||
*/
|
||||
return value < eventsPerMinute ? value : eventsPerMinute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes old data from the history.
|
||||
*
|
||||
* @param now the current system time
|
||||
* @return true if the remaining history is not empty.
|
||||
*/
|
||||
private boolean removeOldHistory(long now) {
|
||||
CadenceData historyElement = history.peek();
|
||||
while (historyElement != null) {
|
||||
if (now - historyElement.systemTime <= MAX_HISTORY_TIME_IN_MILLIS
|
||||
&& history.size() < MAX_HISTORY_SIZE) {
|
||||
return true;
|
||||
}
|
||||
history.removeFirst();
|
||||
historyElement = history.peek();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.google.android.apps.mytracks.services.sensors.ant.AntSensorManager.ChannelStates;
|
||||
|
||||
/**
|
||||
* Ant channel configuration.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public abstract class ChannelConfiguration {
|
||||
|
||||
public static final byte FREQUENCY = 57; // 2457Mhz (Ant+ frequency)
|
||||
public static final byte TRANSMISSION_TYPE = 0; // 0 for wild card search
|
||||
public static final byte PROXIMITY_SEARCH = 7;
|
||||
|
||||
private short deviceNumber = AntSensorManager.WILDCARD;
|
||||
private ChannelStates channelState = ChannelStates.CLOSED;
|
||||
private boolean isInitializing = false;
|
||||
private boolean isDeinitializing = false;
|
||||
|
||||
/**
|
||||
* Gets the My Tracks settings device id key.
|
||||
*/
|
||||
public abstract int getDeviceIdKey();
|
||||
|
||||
/**
|
||||
* Gets the device type.
|
||||
*/
|
||||
public abstract byte getDeviceType();
|
||||
|
||||
/**
|
||||
* Gets the message period.
|
||||
*/
|
||||
public abstract short getMessagPeriod();
|
||||
|
||||
/**
|
||||
* Decodes the message and places the value in antSensorValue.
|
||||
*
|
||||
* @param message the message
|
||||
* @param antSensorValue the ant sensor value
|
||||
*/
|
||||
public abstract void decodeMessage(byte[] message, AntSensorValue antSensorValue);
|
||||
|
||||
/**
|
||||
* Gets the device number.
|
||||
*/
|
||||
public short getDeviceNumber() {
|
||||
return deviceNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the device number.
|
||||
*
|
||||
* @param deviceNumber the device number to set
|
||||
*/
|
||||
public void setDeviceNumber(short deviceNumber) {
|
||||
this.deviceNumber = deviceNumber;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the channel state.
|
||||
*/
|
||||
public ChannelStates getChannelState() {
|
||||
return channelState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the channel state.
|
||||
*
|
||||
* @param channelState the channelState to set
|
||||
*/
|
||||
public void setChannelState(ChannelStates channelState) {
|
||||
this.channelState = channelState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if initializing.
|
||||
*/
|
||||
public boolean isInitializing() {
|
||||
return isInitializing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets isInitializing.
|
||||
*
|
||||
* @param isInitializing true if isInitializing
|
||||
*/
|
||||
public void setInitializing(boolean isInitializing) {
|
||||
this.isInitializing = isInitializing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if deinitializing.
|
||||
*/
|
||||
public boolean isDeinitializing() {
|
||||
return isDeinitializing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets isDeinitializing.
|
||||
*
|
||||
* @param isDeinitializing true if isDeinitializing
|
||||
*/
|
||||
public void setDeinitializing(boolean isDeinitializing) {
|
||||
this.isDeinitializing = isDeinitializing;
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
/**
|
||||
* Combined bike sensor channel configuration.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class CombinedBikeChannelConfiguration extends ChannelConfiguration {
|
||||
|
||||
private static final int DEVICE_ID_KEY = R.string.ant_combined_bike_sensor_id_key;
|
||||
private static final byte DEVICE_TYPE = 0x79;
|
||||
private static final short MESSAGE_PERIOD = 8086;
|
||||
|
||||
private CadenceCounter cadenceCounter = new CadenceCounter();
|
||||
|
||||
@Override
|
||||
public int getDeviceIdKey() {
|
||||
return DEVICE_ID_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getDeviceType() {
|
||||
return DEVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getMessagPeriod() {
|
||||
return MESSAGE_PERIOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
|
||||
int revolutionCount = ((int) message[5] & 0xFF) + ((int) message[6] & 0xFF) * 256;
|
||||
int eventTime = ((int) message[3] & 0xFF) + ((int) message[4] & 0xFF) * 256;
|
||||
|
||||
antSensorValue.setCadence(cadenceCounter.getEventsPerMinute(revolutionCount, eventTime));
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
/**
|
||||
* Heart rate monitor channel configuration.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class HeartRateChannelConfiguration extends ChannelConfiguration {
|
||||
|
||||
private static final int DEVICE_ID_KEY = R.string.ant_heart_rate_monitor_id_key;
|
||||
private static final byte DEVICE_TYPE = 0x78;
|
||||
private static final short MESSAGE_PERIOD = 8070;
|
||||
|
||||
@Override
|
||||
public int getDeviceIdKey() {
|
||||
return DEVICE_ID_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getDeviceType() {
|
||||
return DEVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getMessagPeriod() {
|
||||
return MESSAGE_PERIOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
|
||||
antSensorValue.setHeartRate(message[10] & 0xFF);
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.sensors.ant;
|
||||
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
/**
|
||||
* Speed distance monitor channel configuration.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SpeedDistanceChannelConfiguration extends ChannelConfiguration {
|
||||
|
||||
private static final int DEVICE_ID_KEY = R.string.ant_speed_distance_monitor_id_key;
|
||||
private static final byte DEVICE_TYPE = 0x7C;
|
||||
private static final short MESSAGE_PERIOD = 8134;
|
||||
|
||||
@Override
|
||||
public int getDeviceIdKey() {
|
||||
return DEVICE_ID_KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getDeviceType() {
|
||||
return DEVICE_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public short getMessagPeriod() {
|
||||
return MESSAGE_PERIOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decodeMessage(byte[] message, AntSensorValue antSensorValue) {
|
||||
// Check page 2 data
|
||||
if (message[3] == 0x02) {
|
||||
antSensorValue.setCadence(
|
||||
(int) ((message[6] & 0xFF) + (((message[7] >>> 4) & 0x0F) / 16.0f)));
|
||||
}
|
||||
}
|
||||
}
|
||||
+333
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.StringUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AnnouncementPeriodicTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
private static final String TAG = AnnouncementPeriodicTask.class.getSimpleName();
|
||||
private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
|
||||
|
||||
@VisibleForTesting
|
||||
static final HashMap<String, String> SPEECH_PARAMS = new HashMap<String, String>();
|
||||
static {
|
||||
SPEECH_PARAMS.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "not_used");
|
||||
}
|
||||
|
||||
private final OnUtteranceCompletedListener
|
||||
utteranceListener = new OnUtteranceCompletedListener() {
|
||||
@Override
|
||||
public void onUtteranceCompleted(String utteranceId) {
|
||||
int result = audioManager.abandonAudioFocus(null);
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
Log.w(TAG, "Failed to relinquish audio focus.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
private TextToSpeech tts;
|
||||
|
||||
// Response from TTS after its initialization
|
||||
private int initStatus = TextToSpeech.ERROR;
|
||||
|
||||
// True if TTS engine is ready
|
||||
private boolean ready = false;
|
||||
|
||||
// True if speech is allowed
|
||||
private boolean speechAllowed;
|
||||
|
||||
private final AudioManager audioManager;
|
||||
|
||||
/**
|
||||
* Listener which updates {@link #speechAllowed} when the phone state changes.
|
||||
*/
|
||||
private final PhoneStateListener phoneStateListener = new PhoneStateListener() {
|
||||
@Override
|
||||
public void onCallStateChanged(int state, String incomingNumber) {
|
||||
speechAllowed = state == TelephonyManager.CALL_STATE_IDLE;
|
||||
if (!speechAllowed && tts != null && tts.isSpeaking()) {
|
||||
// If we're already speaking, stop it.
|
||||
tts.stop();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public AnnouncementPeriodicTask(Context context) {
|
||||
this.context = context;
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
if (tts == null) {
|
||||
tts = newTextToSpeech(context, new OnInitListener() {
|
||||
@Override
|
||||
public void onInit(int status) {
|
||||
initStatus = status;
|
||||
}
|
||||
});
|
||||
}
|
||||
speechAllowed = true;
|
||||
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService trackRecordingService) {
|
||||
if (trackRecordingService == null) {
|
||||
Log.e(TAG, "TrackRecordingService is null.");
|
||||
return;
|
||||
}
|
||||
announce(trackRecordingService.getTripStatistics());
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs this task.
|
||||
*
|
||||
* @param tripStatistics the trip statistics
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void announce(TripStatistics tripStatistics) {
|
||||
if (tripStatistics == null) {
|
||||
Log.e(TAG, "TripStatistics is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
if (!ready) {
|
||||
ready = initStatus == TextToSpeech.SUCCESS;
|
||||
if (ready) {
|
||||
onTtsReady();
|
||||
}
|
||||
}
|
||||
if (!ready) {
|
||||
Log.i(TAG, "TTS not ready.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!speechAllowed) {
|
||||
Log.i(TAG, "Speech is not allowed at this time.");
|
||||
return;
|
||||
}
|
||||
speakAnnouncement(getAnnouncement(tripStatistics));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
listenToPhoneState(phoneStateListener, PhoneStateListener.LISTEN_NONE);
|
||||
if (tts != null) {
|
||||
tts.shutdown();
|
||||
tts = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when TTS is ready.
|
||||
*/
|
||||
private void onTtsReady() {
|
||||
Locale locale = Locale.getDefault();
|
||||
int languageAvailability = tts.isLanguageAvailable(locale);
|
||||
if (languageAvailability == TextToSpeech.LANG_MISSING_DATA
|
||||
|| languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) {
|
||||
Log.w(TAG, "Default locale not available, use English.");
|
||||
locale = Locale.ENGLISH;
|
||||
/*
|
||||
* TODO: instead of using english, load the language if missing and show a
|
||||
* toast if not supported. Not able to change the resource strings to
|
||||
* English.
|
||||
*/
|
||||
}
|
||||
tts.setLanguage(locale);
|
||||
|
||||
// Slow down the speed just a bit as it is hard to hear when exercising.
|
||||
tts.setSpeechRate(TTS_SPEECH_RATE);
|
||||
|
||||
tts.setOnUtteranceCompletedListener(utteranceListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaks the announcement.
|
||||
*
|
||||
* @param announcement the announcement
|
||||
*/
|
||||
private void speakAnnouncement(String announcement) {
|
||||
int result = audioManager.requestAudioFocus(
|
||||
null, TextToSpeech.Engine.DEFAULT_STREAM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK);
|
||||
if (result == AudioManager.AUDIOFOCUS_REQUEST_FAILED) {
|
||||
Log.w(TAG, "Failed to request audio focus.");
|
||||
}
|
||||
|
||||
/*
|
||||
* We don't care about the utterance id. It is supplied here to force
|
||||
* onUtteranceCompleted to be called.
|
||||
*/
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, SPEECH_PARAMS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TextToSpeech}.
|
||||
*
|
||||
* @param aContext a context
|
||||
* @param onInitListener an on init listener
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected TextToSpeech newTextToSpeech(Context aContext, OnInitListener onInitListener) {
|
||||
return new TextToSpeech(aContext, onInitListener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the announcement.
|
||||
*
|
||||
* @param tripStatistics the trip statistics
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected String getAnnouncement(TripStatistics tripStatistics) {
|
||||
boolean metricUnits = PreferencesUtils.isMetricUnits(context);
|
||||
boolean reportSpeed = PreferencesUtils.isReportSpeed(context);
|
||||
double distance = tripStatistics.getTotalDistance() * UnitConversions.M_TO_KM;
|
||||
double speed = tripStatistics.getAverageMovingSpeed() * UnitConversions.MS_TO_KMH;
|
||||
|
||||
if (distance == 0) {
|
||||
return context.getString(R.string.voice_total_distance_zero);
|
||||
}
|
||||
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
speed *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
String rate;
|
||||
if (reportSpeed) {
|
||||
int speedId = metricUnits ? R.plurals.voiceSpeedKilometersPerHour
|
||||
: R.plurals.voiceSpeedMilesPerHour;
|
||||
rate = context.getResources().getQuantityString(speedId, getQuantityCount(speed), speed);
|
||||
} else {
|
||||
speed = speed == 0 ? 0.0 : 1 / speed;
|
||||
int paceId = metricUnits ? R.string.voice_pace_per_kilometer : R.string.voice_pace_per_mile;
|
||||
rate = context.getString(paceId, getAnnounceTime((long) (speed * HOUR_TO_MILLISECOND)));
|
||||
}
|
||||
|
||||
int totalDistanceId = metricUnits ? R.plurals.voiceTotalDistanceKilometers
|
||||
: R.plurals.voiceTotalDistanceMiles;
|
||||
String totalDistance = context.getResources()
|
||||
.getQuantityString(totalDistanceId, getQuantityCount(distance), distance);
|
||||
|
||||
return context.getString(R.string.voice_template, totalDistance,
|
||||
getAnnounceTime(tripStatistics.getMovingTime()), rate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens to phone state.
|
||||
*
|
||||
* @param listener the listener
|
||||
* @param events the interested events
|
||||
*/
|
||||
@VisibleForTesting
|
||||
protected void listenToPhoneState(PhoneStateListener listener, int events) {
|
||||
TelephonyManager telephony = (TelephonyManager) context.getSystemService(
|
||||
Context.TELEPHONY_SERVICE);
|
||||
if (telephony != null) {
|
||||
telephony.listen(listener, events);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the announce time.
|
||||
*
|
||||
* @param time the time
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String getAnnounceTime(long time) {
|
||||
int[] parts = StringUtils.getTimeParts(time);
|
||||
String seconds = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceSeconds, parts[0], parts[0]);
|
||||
String minutes = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceMinutes, parts[1], parts[1]);
|
||||
String hours = context.getResources()
|
||||
.getQuantityString(R.plurals.voiceHours, parts[2], parts[2]);
|
||||
StringBuilder sb = new StringBuilder();
|
||||
if (parts[2] != 0) {
|
||||
sb.append(hours);
|
||||
sb.append(" ");
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
} else {
|
||||
sb.append(minutes);
|
||||
sb.append(" ");
|
||||
sb.append(seconds);
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the plural count to be used by getQuantityString. getQuantityString
|
||||
* only supports integer quantities, not a double quantity like "2.2".
|
||||
* <p>
|
||||
* As a temporary workaround, we convert a double quantity to an integer
|
||||
* quantity. If the double quantity is exactly 0, 1, or 2, then we can return
|
||||
* these integer quantities. Otherwise, we cast the double quantity to an
|
||||
* integer quantity. However, we need to make sure that if the casted value is
|
||||
* 0, 1, or 2, we don't return those, instead, return the next biggest integer
|
||||
* 3.
|
||||
*
|
||||
* @param d the double value
|
||||
*/
|
||||
private int getQuantityCount(double d) {
|
||||
if (d == 0) {
|
||||
return 0;
|
||||
} else if (d == 1) {
|
||||
return 1;
|
||||
} else if (d == 2) {
|
||||
return 2;
|
||||
} else {
|
||||
int count = (int) d;
|
||||
return count < 3 ? 3 : count;
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A {@link PeriodicTaskFactory} for text-to-speech announcement periodic task.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class AnnouncementPeriodicTaskFactory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return new AnnouncementPeriodicTask(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2013 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.util.PhotoUtils;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import android.media.ExifInterface;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* A bitmap loader.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class BitmapLoader extends AsyncTask<Void, Void, Bitmap> {
|
||||
private static final String TAG = BitmapLoader.class.getSimpleName();
|
||||
|
||||
private final WeakReference<ImageView> imageViewReference;
|
||||
private final Uri uri;
|
||||
private final int targetWidth;
|
||||
private final int targetHeight;
|
||||
private final boolean fitWithin;
|
||||
|
||||
public BitmapLoader(
|
||||
ImageView imageView, Uri uri, int targetWidth, int targetHeight, boolean fitWithin) {
|
||||
|
||||
// Use a WeakReference to ensure the ImageView can be garbage collected
|
||||
imageViewReference = new WeakReference<ImageView>(imageView);
|
||||
this.uri = uri;
|
||||
this.targetWidth = targetWidth;
|
||||
this.targetHeight = targetHeight;
|
||||
this.fitWithin = fitWithin;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(Void... params) {
|
||||
|
||||
// Get the image dimensions
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(uri.getPath(), options);
|
||||
|
||||
if (options.outWidth == 0 || options.outHeight == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Set imageWidth and imageHeight based on image rotation
|
||||
int rotation = getRotation();
|
||||
int imageWidth;
|
||||
int imageHeight;
|
||||
|
||||
if (rotation == 0 || rotation == 180) {
|
||||
imageWidth = options.outWidth;
|
||||
imageHeight = options.outHeight;
|
||||
} else {
|
||||
imageWidth = options.outHeight;
|
||||
imageHeight = options.outWidth;
|
||||
}
|
||||
|
||||
// Get a scaled down version of the image
|
||||
options.inJustDecodeBounds = false;
|
||||
options.inSampleSize = getInSampleSize(imageWidth, imageHeight);
|
||||
options.inPurgeable = true;
|
||||
|
||||
Bitmap scaledBitmap = BitmapFactory.decodeFile(uri.getPath(), options);
|
||||
if (scaledBitmap == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get the final bitmap after rotating the scaled down image
|
||||
Bitmap bitmap;
|
||||
if (rotation == 0 && fitWithin) {
|
||||
bitmap = scaledBitmap;
|
||||
} else {
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.postRotate(rotation);
|
||||
int offset = 0;
|
||||
int height = scaledBitmap.getHeight();
|
||||
if (!fitWithin && height > targetHeight) {
|
||||
offset = (height - targetHeight) / 2;
|
||||
height = targetHeight;
|
||||
}
|
||||
bitmap = Bitmap.createBitmap(
|
||||
scaledBitmap, 0, offset, scaledBitmap.getWidth(), height, matrix, true);
|
||||
scaledBitmap.recycle();
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
if (isCancelled()) {
|
||||
bitmap = null;
|
||||
}
|
||||
// If imageView is still around, set bitmap
|
||||
if (imageViewReference != null && bitmap != null) {
|
||||
ImageView imageView = imageViewReference.get();
|
||||
if (imageView != null) {
|
||||
BitmapLoader bitmapLoader = PhotoUtils.getBitmapLoader(imageView);
|
||||
if (this == bitmapLoader) {
|
||||
imageView.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int getRotation() {
|
||||
try {
|
||||
ExifInterface exifInterface = new ExifInterface(uri.getPath());
|
||||
switch (exifInterface.getAttributeInt(
|
||||
ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
return 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
return 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
return 270;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Unable to get photo orientation", e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the in sample size.
|
||||
*
|
||||
* @param imageWidth the image width
|
||||
* @param imageHeight the image height
|
||||
*/
|
||||
private int getInSampleSize(int imageWidth, int imageHeight) {
|
||||
float widthRatio = 1;
|
||||
if (imageWidth > targetWidth) {
|
||||
widthRatio = (float) imageWidth / (float) targetWidth;
|
||||
}
|
||||
|
||||
float heightRatio = 1;
|
||||
if (imageHeight > targetHeight) {
|
||||
heightRatio = (float) imageHeight / (float) targetHeight;
|
||||
}
|
||||
|
||||
double size;
|
||||
if (fitWithin) {
|
||||
/*
|
||||
* To fit within the target area, return the larger sample ratio so the
|
||||
* image will not be larger than the target dimensions.
|
||||
*/
|
||||
size = Math.max(widthRatio, heightRatio);
|
||||
} else {
|
||||
/*
|
||||
* To fill the target area, return the smaller ratio so the image will
|
||||
* cover both dimensions.
|
||||
*/
|
||||
size = Math.min(widthRatio, heightRatio);
|
||||
}
|
||||
// Use Math.floor to not under-sample.
|
||||
return (int) Math.floor(size);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
/**
|
||||
* This is interface for a task that will be executed on some schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTask {
|
||||
/**
|
||||
* Sets up this task for subsequent calls to the run method.
|
||||
*/
|
||||
public void start();
|
||||
|
||||
/**
|
||||
* This method will be called periodically.
|
||||
*
|
||||
* @param trackRecordingService the track recording service
|
||||
*/
|
||||
public void run(TrackRecordingService trackRecordingService);
|
||||
|
||||
/**
|
||||
* Shuts down this task and clean up resources.
|
||||
*/
|
||||
public void shutdown();
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.apps.mytracks.util.UnitConversions;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* Execute a periodic task on a time or distance schedule.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PeriodicTaskExecutor {
|
||||
|
||||
private static final String TAG = PeriodicTaskExecutor.class.getSimpleName();
|
||||
private static final long MINUTE_TO_MILLISECONDS = 60000L;
|
||||
|
||||
private final TrackRecordingService trackRecordingService;
|
||||
private final PeriodicTaskFactory periodicTaskFactory;
|
||||
|
||||
/**
|
||||
* The task frequency. A positive value is a time frequency (minutes). A
|
||||
* negative value is a distance frequency (km or mi). A zero value is to turn
|
||||
* off periodic task.
|
||||
*/
|
||||
private int taskFrequency = PreferencesUtils.FREQUENCY_OFF;
|
||||
|
||||
private PeriodicTask periodicTask;
|
||||
|
||||
// Time periodic task executor
|
||||
private TimerTaskExecutor timerTaskExecutor = null;
|
||||
|
||||
private boolean metricUnits;
|
||||
|
||||
// The next distance for the distance periodic task
|
||||
private double nextTaskDistance = Double.MAX_VALUE;
|
||||
|
||||
public PeriodicTaskExecutor(
|
||||
TrackRecordingService trackRecordingService, PeriodicTaskFactory periodicTaskFactory) {
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
this.periodicTaskFactory = periodicTaskFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the executor.
|
||||
*/
|
||||
public void restore() {
|
||||
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
|
||||
Log.d(TAG, "Not recording or paused.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTimeFrequency()) {
|
||||
if (timerTaskExecutor != null) {
|
||||
timerTaskExecutor.shutdown();
|
||||
timerTaskExecutor = null;
|
||||
}
|
||||
}
|
||||
if (taskFrequency == PreferencesUtils.FREQUENCY_OFF) {
|
||||
Log.d(TAG, "Task frequency is off.");
|
||||
return;
|
||||
}
|
||||
|
||||
periodicTask = periodicTaskFactory.create(trackRecordingService);
|
||||
|
||||
// Returning null is ok
|
||||
if (periodicTask == null) {
|
||||
Log.d(TAG, "Peridoic task is null.");
|
||||
return;
|
||||
}
|
||||
periodicTask.start();
|
||||
|
||||
if (isTimeFrequency()) {
|
||||
if (timerTaskExecutor == null) {
|
||||
timerTaskExecutor = new TimerTaskExecutor(periodicTask, trackRecordingService);
|
||||
}
|
||||
timerTaskExecutor.scheduleTask(taskFrequency * MINUTE_TO_MILLISECONDS);
|
||||
} else {
|
||||
// For distance periodic task
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down the executor.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (periodicTask != null) {
|
||||
periodicTask.shutdown();
|
||||
periodicTask = null;
|
||||
}
|
||||
if (timerTaskExecutor != null) {
|
||||
timerTaskExecutor.shutdown();
|
||||
timerTaskExecutor = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the executor.
|
||||
*/
|
||||
public void update() {
|
||||
if (!isDistanceFrequency() || periodicTask == null) {
|
||||
return;
|
||||
}
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
double distance = tripStatistics.getTotalDistance()
|
||||
* UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
|
||||
if (distance > nextTaskDistance) {
|
||||
periodicTask.run(trackRecordingService);
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets task frequency.
|
||||
*
|
||||
* @param taskFrequency the task frequency
|
||||
*/
|
||||
public void setTaskFrequency(int taskFrequency) {
|
||||
this.taskFrequency = taskFrequency;
|
||||
restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets metricUnits.
|
||||
*
|
||||
* @param metricUnits true to use metric units
|
||||
*/
|
||||
public void setMetricUnits(boolean metricUnits) {
|
||||
this.metricUnits = metricUnits;
|
||||
calculateNextTaskDistance();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the next distance for the distance periodic task.
|
||||
*/
|
||||
private void calculateNextTaskDistance() {
|
||||
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()
|
||||
|| periodicTask == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDistanceFrequency()) {
|
||||
nextTaskDistance = Double.MAX_VALUE;
|
||||
Log.d(TAG, "SplitManager: Distance splits disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
double distance = tripStatistics.getTotalDistance()
|
||||
* UnitConversions.M_TO_KM;
|
||||
if (!metricUnits) {
|
||||
distance *= UnitConversions.KM_TO_MI;
|
||||
}
|
||||
// The index will be negative since the frequency is negative.
|
||||
int index = (int) (distance / taskFrequency);
|
||||
index -= 1;
|
||||
nextTaskDistance = taskFrequency * index;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if time frequency.
|
||||
*/
|
||||
private boolean isTimeFrequency() {
|
||||
return taskFrequency > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if distance frequency.
|
||||
*/
|
||||
private boolean isDistanceFrequency() {
|
||||
return taskFrequency < 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* An interface for classes that can create {@link PeriodicTask}.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTaskFactory {
|
||||
|
||||
/**
|
||||
* Creates a {@link PeriodicTask}.
|
||||
*
|
||||
* @return the task, or null if the task is not supported
|
||||
*/
|
||||
public PeriodicTask create(Context context);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2010 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.content.WaypointCreationRequest;
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers periodically.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SplitPeriodicTask implements PeriodicTask {
|
||||
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService trackRecordingService) {
|
||||
trackRecordingService.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Copyright 2012 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A {@link PeriodicTaskFactory} for {@link SplitPeriodicTask}.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class SplitPeriodicTaskFactory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return new SplitPeriodicTask();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2009 Google Inc.
|
||||
*
|
||||
* 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.google.android.apps.mytracks.services.tasks;
|
||||
|
||||
import com.google.android.apps.mytracks.services.TrackRecordingService;
|
||||
import com.google.android.apps.mytracks.stats.TripStatistics;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
/**
|
||||
* This class will periodically perform a task.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class TimerTaskExecutor {
|
||||
|
||||
private final PeriodicTask periodicTask;
|
||||
private final TrackRecordingService trackRecordingService;
|
||||
|
||||
private TimerTask timerTask;
|
||||
private Timer timer;
|
||||
|
||||
public TimerTaskExecutor(PeriodicTask periodicTask, TrackRecordingService trackRecordingService) {
|
||||
this.periodicTask = periodicTask;
|
||||
this.trackRecordingService = trackRecordingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules the periodic task at an interval.
|
||||
*
|
||||
* @param interval the interval in milliseconds
|
||||
*/
|
||||
public void scheduleTask(long interval) {
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trackRecordingService.isRecording() || trackRecordingService.isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
TripStatistics tripStatistics = trackRecordingService.getTripStatistics();
|
||||
if (tripStatistics == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
shutdown();
|
||||
periodicTask.start();
|
||||
timerTask = new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
periodicTask.run(trackRecordingService);
|
||||
}
|
||||
};
|
||||
timer = new Timer(TimerTaskExecutor.class.getSimpleName());
|
||||
long next = System.currentTimeMillis() + interval - (tripStatistics.getTotalTime() % interval);
|
||||
timer.scheduleAtFixedRate(timerTask, new Date(next), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (timerTask != null) {
|
||||
timerTask.cancel();
|
||||
timerTask = null;
|
||||
}
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
}
|
||||
periodicTask.shutdown();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user