forked from upstream-mirrors/OpenTracks
Add a class EndToEndTestRunner which extends InstrumentationTestRunner and can get the port of emulator. Then test can be run on more than one emulators at the same time.
This commit is contained in:
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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 {
|
||||
|
||||
/**
|
||||
* The interval to request for gps signal.
|
||||
*/
|
||||
private final long interval;
|
||||
|
||||
/**
|
||||
* @param interval The interval to request for gps signal
|
||||
*/
|
||||
public AbsoluteLocationListenerPolicy(final long interval) {
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The interval given in the constructor
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
return interval;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards the idle time.
|
||||
*/
|
||||
public void updateIdleTime(long idleTime) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
* Get all updates to properly measure moving time.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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 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 whic is half of
|
||||
* the idle time.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class AdaptiveLocationListenerPolicy implements LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Smallest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long minInterval;
|
||||
|
||||
/**
|
||||
* Largest interval this policy will dictate, in milliseconds.
|
||||
*/
|
||||
private final long maxInterval;
|
||||
|
||||
private final int minDistance;
|
||||
|
||||
/**
|
||||
* The time the user has been at the current location, in milliseconds.
|
||||
*/
|
||||
private long idleTime;
|
||||
|
||||
/**
|
||||
* Creates a policy that will be bounded by the given min and max.
|
||||
*
|
||||
* @param min Smallest interval this policy will dictate, in milliseconds
|
||||
* @param max Largest interval this policy will dictate, in milliseconds
|
||||
*/
|
||||
public AdaptiveLocationListenerPolicy(long min, long max, int minDistance) {
|
||||
this.minInterval = min;
|
||||
this.maxInterval = max;
|
||||
this.minDistance = minDistance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return An interval bounded by min and max which is half of the idle time
|
||||
*/
|
||||
public long getDesiredPollingInterval() {
|
||||
long desiredInterval = idleTime / 2;
|
||||
// Round to avoid setting the interval too often.
|
||||
desiredInterval = (desiredInterval / 1000) * 1000;
|
||||
return Math.max(Math.min(maxInterval, desiredInterval),
|
||||
minInterval);
|
||||
}
|
||||
|
||||
public void updateIdleTime(long newIdleTime) {
|
||||
this.idleTime = newIdleTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance() {
|
||||
return minDistance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
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 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 trackRecordingService the trackRecordingService
|
||||
*/
|
||||
@VisibleForTesting
|
||||
void onHandleIntent(Intent intent, @SuppressWarnings("hiding") ITrackRecordingService trackRecordingService) {
|
||||
String action = intent.getAction();
|
||||
if (action != null) {
|
||||
try {
|
||||
if (action.equals(getString(R.string.track_action_start))) {
|
||||
trackRecordingService.startNewTrack();
|
||||
} else if (action.equals(getString(R.string.track_action_end))) {
|
||||
trackRecordingService.endCurrentTrack();
|
||||
}
|
||||
} 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,48 @@
|
||||
/*
|
||||
* 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:
|
||||
* Absolute
|
||||
* Addaptive
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface LocationListenerPolicy {
|
||||
|
||||
/**
|
||||
* Returns the polling time this policy would like at this time.
|
||||
*
|
||||
* @return The polling that this policy dictates
|
||||
*/
|
||||
public long getDesiredPollingInterval();
|
||||
|
||||
/**
|
||||
* Returns the minimum distance between updates.
|
||||
*/
|
||||
public int getMinDistance();
|
||||
|
||||
/**
|
||||
* Notifies the amount of time the user has been idle at their current
|
||||
* location.
|
||||
*
|
||||
* @param idleTime The time that the user has been idle at this spot
|
||||
*/
|
||||
public void updateIdleTime(long idleTime);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import com.google.android.apps.mytracks.Constants;
|
||||
import com.google.android.apps.mytracks.util.PreferencesUtils;
|
||||
import com.google.android.maps.mytracks.R;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* A class that manages reading the shared preferences for the service.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class PreferenceManager implements OnSharedPreferenceChangeListener {
|
||||
private TrackRecordingService service;
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public PreferenceManager(TrackRecordingService service) {
|
||||
this.service = service;
|
||||
this.sharedPreferences = service.getSharedPreferences(
|
||||
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
|
||||
// Refresh all properties.
|
||||
onSharedPreferenceChanged(sharedPreferences, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Notifies that preferences have changed. Call this with key == null to
|
||||
* update all preferences in one call.
|
||||
*
|
||||
* @param key the key that changed (may be null to update all preferences)
|
||||
*/
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
|
||||
if (service == null) {
|
||||
Log.w(Constants.TAG, "onSharedPreferenceChanged: a preference change (key = " + key
|
||||
+ ") after a call to shutdown()");
|
||||
return;
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.recording_track_id_key).equals(key)) {
|
||||
long recordingTrackId = PreferencesUtils.getLong(service, R.string.recording_track_id_key);
|
||||
/*
|
||||
* Only set the id if it is valid. Setting it to -1L should only happen
|
||||
* in TrackRecordingService.endCurrentTrack()
|
||||
*/
|
||||
if (recordingTrackId != PreferencesUtils.RECORDING_TRACK_ID_DEFAULT) {
|
||||
service.setRecordingTrackId(recordingTrackId);
|
||||
}
|
||||
}
|
||||
if (key == null || PreferencesUtils.getKey(service, R.string.metric_units_key).equals(key)) {
|
||||
service.setMetricUnits(PreferencesUtils.getBoolean(
|
||||
service, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.announcement_frequency_key).equals(key)) {
|
||||
service.setAnnouncementFrequency(PreferencesUtils.getInt(
|
||||
service, R.string.announcement_frequency_key,
|
||||
PreferencesUtils.ANNOUNCEMENT_FREQUENCY_DEFAULT));
|
||||
}
|
||||
if (key == null || PreferencesUtils.getKey(service, R.string.split_frequency_key).equals(key)) {
|
||||
service.setSplitFrequency(PreferencesUtils.getInt(
|
||||
service, R.string.split_frequency_key, PreferencesUtils.SPLIT_FREQUENCY_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_recording_interval_key).equals(key)) {
|
||||
int minRecordingInterval = PreferencesUtils.getInt(service,
|
||||
R.string.min_recording_interval_key, PreferencesUtils.MIN_RECORDING_INTERVAL_DEFAULT);
|
||||
switch (minRecordingInterval) {
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_BATTERY_LIFE:
|
||||
/*
|
||||
* Choose battery life over moving time accuracy. min: 30 seconds,
|
||||
* max: 5 minutes, inDist: 5 meters.
|
||||
*/
|
||||
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(30000, 300000, 5));
|
||||
break;
|
||||
case PreferencesUtils.MIN_RECORDING_INTERVAL_ADAPT_ACCURACY:
|
||||
/*
|
||||
* Get all the updates. min: 1 second, max: 30 seconds, minDist: 0
|
||||
* meter.
|
||||
*/
|
||||
service.setLocationListenerPolicy(new AdaptiveLocationListenerPolicy(1000, 30000, 0));
|
||||
break;
|
||||
default:
|
||||
service.setLocationListenerPolicy(
|
||||
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000));
|
||||
}
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_recording_distance_key).equals(key)) {
|
||||
service.setMinRecordingDistance(PreferencesUtils.getInt(
|
||||
service, R.string.min_recording_distance_key,
|
||||
PreferencesUtils.MIN_RECORDING_DISTANCE_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.max_recording_distance_key).equals(key)) {
|
||||
service.setMaxRecordingDistance(PreferencesUtils.getInt(
|
||||
service, R.string.max_recording_distance_key,
|
||||
PreferencesUtils.MAX_RECORDING_DISTANCE_DEFAULT));
|
||||
}
|
||||
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.min_required_accuracy_key).equals(key)) {
|
||||
service.setMinRequiredAccuracy(PreferencesUtils.getInt(
|
||||
service, R.string.min_required_accuracy_key,
|
||||
PreferencesUtils.MIN_REQUIRED_ACCURACY_DEFAULT));
|
||||
}
|
||||
if (key == null
|
||||
|| PreferencesUtils.getKey(service, R.string.auto_resume_track_timeout_key).equals(key)) {
|
||||
service.setAutoResumeTrackTimeout(PreferencesUtils.getInt(
|
||||
service, R.string.auto_resume_track_timeout_key,
|
||||
PreferencesUtils.AUTO_RESUME_TRACK_TIMEOUT_DEFAULT));
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
sharedPreferences.unregisterOnSharedPreferenceChangeListener(this);
|
||||
service = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* 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.io.file.TrackWriterFactory.TrackFileFormat;
|
||||
import com.google.android.apps.mytracks.util.FileUtils;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* A service to remove My Tracks temp files older than one hour on the SD card.
|
||||
*
|
||||
* @author Jimmy Shih
|
||||
*/
|
||||
public class RemoveTempFilesService extends Service {
|
||||
|
||||
private static final String TAG = RemoveTempFilesService.class.getSimpleName();
|
||||
private static final int ONE_HOUR_IN_MILLISECONDS = 60 * 60 * 1000;
|
||||
|
||||
private RemoveTempFilesAsyncTask removeTempFilesAsyncTask = null;
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
|
||||
// Setup an alarm to repeatedly call this service
|
||||
Intent alarmIntent = new Intent(this, RemoveTempFilesService.class);
|
||||
PendingIntent pendingIntent = PendingIntent.getService(
|
||||
this, 0, alarmIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP,
|
||||
System.currentTimeMillis() + ONE_HOUR_IN_MILLISECONDS, AlarmManager.INTERVAL_HOUR,
|
||||
pendingIntent);
|
||||
|
||||
// Invoke the AsyncTask to cleanup the temp files
|
||||
if (removeTempFilesAsyncTask == null
|
||||
|| removeTempFilesAsyncTask.getStatus().equals(AsyncTask.Status.FINISHED)) {
|
||||
removeTempFilesAsyncTask = new RemoveTempFilesAsyncTask();
|
||||
removeTempFilesAsyncTask.execute((Void[]) null);
|
||||
}
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
private class RemoveTempFilesAsyncTask extends AsyncTask<Void, Void, Void> {
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
||||
// Can't do anything
|
||||
return null;
|
||||
}
|
||||
cleanTempDirectory(TrackFileFormat.GPX.getExtension());
|
||||
cleanTempDirectory(TrackFileFormat.KML.getExtension());
|
||||
cleanTempDirectory(TrackFileFormat.CSV.getExtension());
|
||||
cleanTempDirectory(TrackFileFormat.TCX.getExtension());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Void result) {
|
||||
stopSelf();
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanTempDirectory(String name) {
|
||||
cleanTempDirectory(new File(FileUtils.buildExternalDirectoryPath(name, "tmp")));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes temp files in a directory older than one hour.
|
||||
*
|
||||
* @param dir the directory
|
||||
* @return the number of files removed.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
int cleanTempDirectory(File dir) {
|
||||
if (!dir.exists()) {
|
||||
return 0;
|
||||
}
|
||||
int count = 0;
|
||||
long oneHourAgo = System.currentTimeMillis() - ONE_HOUR_IN_MILLISECONDS;
|
||||
for (File f : dir.listFiles()) {
|
||||
if (f.lastModified() < oneHourAgo) {
|
||||
if (!f.delete()) {
|
||||
Log.e(TAG, "Unable to delete file: " + f.getAbsolutePath());
|
||||
} else {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+172
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* 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 static com.google.android.apps.mytracks.Constants.TAG;
|
||||
|
||||
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 connection to the track recording service.
|
||||
* This handles connection/disconnection internally, only returning a real
|
||||
* service for use if one is available and connected.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class TrackRecordingServiceConnection {
|
||||
private ITrackRecordingService boundService;
|
||||
|
||||
private final DeathRecipient deathRecipient = new DeathRecipient() {
|
||||
@Override
|
||||
public void binderDied() {
|
||||
Log.d(TAG, "Service died");
|
||||
setBoundService(null);
|
||||
}
|
||||
};
|
||||
|
||||
private final ServiceConnection serviceConnection = new ServiceConnection() {
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
Log.i(TAG, "Connected to service");
|
||||
try {
|
||||
service.linkToDeath(deathRecipient, 0);
|
||||
} catch (RemoteException e) {
|
||||
Log.e(TAG, "Failed to bind a death recipient", e);
|
||||
}
|
||||
|
||||
setBoundService(ITrackRecordingService.Stub.asInterface(service));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName className) {
|
||||
Log.i(TAG, "Disconnected from service");
|
||||
setBoundService(null);
|
||||
}
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
|
||||
private final Runnable bindChangedCallback;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param context the current context
|
||||
* @param bindChangedCallback a callback to be executed when the state of the
|
||||
* service binding changes
|
||||
*/
|
||||
public TrackRecordingServiceConnection(Context context, Runnable bindChangedCallback) {
|
||||
this.context = context;
|
||||
this.bindChangedCallback = bindChangedCallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds to the service, starting it if necessary.
|
||||
*/
|
||||
public void startAndBind() {
|
||||
bindService(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds to the service, only if it's already running.
|
||||
*/
|
||||
public void bindIfRunning() {
|
||||
bindService(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds from and stops the service.
|
||||
*/
|
||||
public void stop() {
|
||||
unbind();
|
||||
|
||||
Log.d(TAG, "Stopping service");
|
||||
Intent intent = new Intent(context, TrackRecordingService.class);
|
||||
context.stopService(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unbinds from the service (but leaves it running).
|
||||
*/
|
||||
public void unbind() {
|
||||
Log.d(TAG, "Unbinding from the service");
|
||||
try {
|
||||
context.unbindService(serviceConnection);
|
||||
} catch (IllegalArgumentException e) {
|
||||
// Means we weren't bound, which is ok.
|
||||
}
|
||||
|
||||
setBoundService(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the service if connected to it, or null if not connected.
|
||||
*/
|
||||
public ITrackRecordingService getServiceIfBound() {
|
||||
checkBindingAlive();
|
||||
|
||||
return boundService;
|
||||
}
|
||||
|
||||
private void checkBindingAlive() {
|
||||
if (boundService != null &&
|
||||
!boundService.asBinder().isBinderAlive()) {
|
||||
setBoundService(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindService(boolean startIfNeeded) {
|
||||
if (boundService != null) {
|
||||
// Already bound.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!startIfNeeded
|
||||
&& !TrackRecordingServiceConnectionUtils.isRecordingServiceRunning(context)) {
|
||||
// Not running, start not requested.
|
||||
Log.d(TAG, "Service not running, not binding to it.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (startIfNeeded) {
|
||||
Log.i(TAG, "Starting the service");
|
||||
Intent intent = new Intent(context, TrackRecordingService.class);
|
||||
context.startService(intent);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Binding to the service");
|
||||
Intent intent = new Intent(context, TrackRecordingService.class);
|
||||
int flags = BuildConfig.DEBUG ? Context.BIND_DEBUG_UNBIND : 0;
|
||||
context.bindService(intent, serviceConnection, flags);
|
||||
}
|
||||
|
||||
private void setBoundService(ITrackRecordingService service) {
|
||||
boundService = service;
|
||||
if (bindChangedCallback != null) {
|
||||
bindChangedCallback.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,130 @@
|
||||
/*
|
||||
* 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();
|
||||
|
||||
/**
|
||||
* A time task to check sensor connection.
|
||||
*/
|
||||
private TimerTask checkSensorConnectionTimeTask = 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:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final Timer timer = new 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();
|
||||
timer.schedule(checkSensorConnectionTimeTask, RETRY_PERIOD, RETRY_PERIOD);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the sensor.
|
||||
*/
|
||||
public void stopSensor() {
|
||||
timer.cancel();
|
||||
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;
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* 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 sensorTypeValueNone = context.getString(R.string.sensor_type_value_none);
|
||||
String sensorType = PreferencesUtils.getString(
|
||||
context, R.string.sensor_type_key, sensorTypeValueNone);
|
||||
|
||||
if (sensorType.equals(context.getString(R.string.sensor_type_value_ant))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, "/sensor/ant");
|
||||
}
|
||||
return new AntSensorManager(context);
|
||||
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_zephyr))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, "/sensor/zephyr");
|
||||
}
|
||||
return new ZephyrSensorManager(context);
|
||||
} else if (sensorType.equals(context.getString(R.string.sensor_type_value_polar))) {
|
||||
if (sendPageViews) {
|
||||
AnalyticsUtils.sendPageViews(context, "/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.settings_sensor_type_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 Ršttsches
|
||||
*/
|
||||
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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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.tasks;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.media.AudioManager;
|
||||
import android.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics. This class
|
||||
* will request and release audio focus.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
@TargetApi(8)
|
||||
public class Api8StatusAnnouncerTask extends StatusAnnouncerTask {
|
||||
|
||||
private static final String TAG = Api8StatusAnnouncerTask.class.getSimpleName();
|
||||
private 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 AudioManager audioManager;
|
||||
|
||||
public Api8StatusAnnouncerTask(Context context) {
|
||||
super(context);
|
||||
audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTtsReady() {
|
||||
super.onTtsReady();
|
||||
tts.setOnUtteranceCompletedListener(utteranceListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected synchronized 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
* 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.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()) {
|
||||
Log.d(TAG, "Not recording.");
|
||||
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;
|
||||
}
|
||||
double distance = trackRecordingService.getTripStatistics().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() || periodicTask == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDistanceFrequency()) {
|
||||
nextTaskDistance = Double.MAX_VALUE;
|
||||
Log.d(TAG, "SplitManager: Distance splits disabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
double distance = trackRecordingService.getTripStatistics().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 periodic tasks.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public interface PeriodicTaskFactory {
|
||||
|
||||
/**
|
||||
* Creates a periodic task.
|
||||
*
|
||||
* @return the task, or null if task is not supported
|
||||
*/
|
||||
public PeriodicTask create(Context context);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* A simple task to insert statistics markers periodically.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class SplitTask implements PeriodicTask {
|
||||
|
||||
private SplitTask() {}
|
||||
|
||||
@Override
|
||||
public void start() {}
|
||||
|
||||
@Override
|
||||
public void run(TrackRecordingService service) {
|
||||
service.insertWaypoint(WaypointCreationRequest.DEFAULT_STATISTICS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {}
|
||||
|
||||
/**
|
||||
* Create new {@link SplitTask}.
|
||||
*/
|
||||
public static class Factory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return new SplitTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
@@ -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 com.google.android.apps.mytracks.util.ApiAdapterFactory;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Factory which wraps construction and setup of text-to-speech announcements in
|
||||
* an API-level-safe way.
|
||||
*
|
||||
* @author Rodrigo Damazio
|
||||
*/
|
||||
public class StatusAnnouncerFactory implements PeriodicTaskFactory {
|
||||
|
||||
@Override
|
||||
public PeriodicTask create(Context context) {
|
||||
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
|
||||
}
|
||||
}
|
||||
+298
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
* 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.speech.tts.TextToSpeech;
|
||||
import android.speech.tts.TextToSpeech.OnInitListener;
|
||||
import android.telephony.PhoneStateListener;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* This class will periodically announce the user's trip statistics.
|
||||
*
|
||||
* @author Sandor Dornbush
|
||||
*/
|
||||
public class StatusAnnouncerTask implements PeriodicTask {
|
||||
|
||||
/**
|
||||
* The rate at which announcements are spoken.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final float TTS_SPEECH_RATE = 0.9f;
|
||||
|
||||
private static final String TAG = StatusAnnouncerTask.class.getSimpleName();
|
||||
private static final long HOUR_TO_MILLISECOND = 60 * 60 * 1000;
|
||||
|
||||
private final Context context;
|
||||
protected 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;
|
||||
|
||||
/**
|
||||
* 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 StatusAnnouncerTask(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@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.
|
||||
*/
|
||||
protected 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaks the announcement.
|
||||
*
|
||||
* @param announcement the announcement
|
||||
*/
|
||||
protected void speakAnnouncement(String announcement) {
|
||||
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.getBoolean(
|
||||
context, R.string.metric_units_key, PreferencesUtils.METRIC_UNITS_DEFAULT);
|
||||
boolean reportSpeed = PreferencesUtils.getBoolean(
|
||||
context, R.string.report_speed_key, PreferencesUtils.REPORT_SPEED_DEFAULT);
|
||||
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);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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 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;
|
||||
|
||||
/**
|
||||
* A timer to schedule the announcements. This is non-null if the task is in
|
||||
* started (scheduled) state.
|
||||
*/
|
||||
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 (!trackRecordingService.isRecording()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
} else {
|
||||
// First start, or we were previously shut down.
|
||||
periodicTask.start();
|
||||
}
|
||||
|
||||
timer = new Timer();
|
||||
if (interval <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long next = trackRecordingService.getTripStatistics().getStartTime();
|
||||
if (next < now) {
|
||||
next = now + interval - ((now - next) % interval);
|
||||
}
|
||||
timer.scheduleAtFixedRate(new PeriodicTimerTask(), new Date(next), interval);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down.
|
||||
*/
|
||||
public void shutdown() {
|
||||
if (timer != null) {
|
||||
timer.cancel();
|
||||
timer.purge();
|
||||
timer = null;
|
||||
periodicTask.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The timer task to announce the trip status.
|
||||
*/
|
||||
private class PeriodicTimerTask extends TimerTask {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
periodicTask.run(trackRecordingService);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user