Merging changes from realtime branch into mainline.

This commit is contained in:
Sandor Dornbush
2010-08-07 10:38:41 -07:00
commit 06e453dd05
204 changed files with 30870 additions and 0 deletions
@@ -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;
}
}
@@ -0,0 +1,81 @@
/*
* 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,96 @@
/*
* Copyright 2008 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.content.Waypoint;
/**
* MyTracks service.
* This service is the process that actually records and manages tracks.
*/
interface ITrackRecordingService {
/**
* Starts recording a new track.
*
* @return the track ID of the new track
*/
long startNewTrack();
/**
* Checks and returns whether we're currently recording a track.
*/
boolean isRecording();
/**
* Returns the track ID of the track currently being recorded, or -1 if none
* is being recorded. This ID can then be used to read track data from the
* content source.
*/
long getRecordingTrackId();
/**
* Inserts a waypoint marker in the track being recorded.
*
* @param waypoint the waypoint to insert
* @return the unique ID of the inserted marker
*/
long insertWaypointMarker(in Waypoint waypoint);
/**
* Inserts a statistics marker in the track being recorded.
*
* @param location the location at which to insert the marker
* @return the unique ID of the inserted marker
*/
long insertStatisticsMarker(in Location location);
/**
* Inserts a location in the track being recorded.
*
* When recording, locations detected by the GPS are already automatically
* added to the track, so this should be used only for adding special points
* or for testing.
*
* @param loc the location to insert
*/
void recordLocation(in Location loc);
/**
* Stops recording the current track.
*/
void endCurrentTrack();
/**
* Returns whether at least one track has been or is being recorded (and was
* not deleted).
*/
boolean hasRecorded();
/**
* Deletes all the stored tracks.
*/
void deleteAllTracks();
/**
* Notifies the service that its preferences may have been changed.
* This is necessary because the service running on a separate process cannot
* listen to the changes itself.
*
* @param key the preference key which may have changed
*/
void sharedPreferenceChanged(in String key);
}
@@ -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,39 @@
/*
* 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 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.
*/
public void run(TrackRecordingService service);
/**
* Shuts down this task and clean up resources.
*/
public void shutdown();
}
@@ -0,0 +1,92 @@
/*
* 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;
import com.google.android.apps.mytracks.MyTracksConstants;
import android.util.Log;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/**
* This class will periodically announce the user's trip statitics.
*
* @author Sandor Dornbush
*/
public class PeriodicTaskExecuter {
private final TrackRecordingService service;
/**
* A timer to schedule the announcements.
*/
private Timer timer = new Timer();
private final PeriodicTask task;
public PeriodicTaskExecuter(PeriodicTask task,
TrackRecordingService service) {
this.task = task;
this.service = service;
}
/**
* Schedules the task at the given interval.
*
* @param interval The interval in milliseconds
*/
public void scheduleTask(long interval) {
timer.cancel();
timer.purge();
timer = new Timer();
if (interval <= 0) {
return;
}
long now = System.currentTimeMillis();
long next = service.getTripStatistics().getStartTime();
while (next < now) next += interval;
Date start = new Date(next);
Log.i(MyTracksConstants.TAG,
"StatusAnnouncer scheduled to start at " + start + " every "
+ interval + " milliseconds.");
timer.scheduleAtFixedRate(new PeriodicTimerTask(), start, interval);
}
/**
* Cleans up this object.
*/
public void shutdown() {
timer.cancel();
timer.purge();
timer = null;
task.shutdown();
}
/**
* The timer task to announce the trip status.
*/
private class PeriodicTimerTask extends TimerTask {
@Override
public void run() {
task.run(service);
}
}
}
@@ -0,0 +1,62 @@
/*
* 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;
import android.content.Context;
/**
* This class will periodically announce the user's trip statitics. This class
* is a wrapper for cupcake devices.
*
* @author Sandor Dornbush
*/
public class SafeStatusAnnouncerTask implements PeriodicTask {
private StatusAnnouncerTask announcer;
/* class initialization fails when this throws an exception */
static {
try {
Class.forName(
"com.google.android.apps.mytracks.services.StatusAnnouncerTask");
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
} catch (LinkageError er) {
throw new RuntimeException(er);
}
}
/* calling here forces class initialization */
public static void checkAvailable() {
}
public SafeStatusAnnouncerTask(Context context) {
announcer = new StatusAnnouncerTask(context);
}
public void run(TrackRecordingService service) {
announcer.run(service);
}
public void shutdown() {
announcer.shutdown();
}
@Override
public void start() {
}
}
@@ -0,0 +1,193 @@
/*
* 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.MyTracksConstants;
import com.google.android.apps.mytracks.content.Waypoint;
import android.content.Context;
import android.location.Location;
import android.telephony.CellLocation;
import android.telephony.NeighboringCellInfo;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import java.util.List;
/**
* A class to monitor the network signal strength.
*
* @author Sandor Dornbush
*/
public class SignalStrengthTask extends PhoneStateListener
implements PeriodicTask {
private final Context context;
private TelephonyManager manager;
private int signalStrength = -1;
public SignalStrengthTask(Context c) {
context = c;
}
public void start() {
manager =
(TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
if (manager == null) {
Log.e(MyTracksConstants.TAG, "Cannot get telephony manager.");
} else {
manager.listen(this, getListenEvents());
}
}
protected int getListenEvents() {
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
}
@Override
public void onSignalStrengthChanged(int asu) {
this.signalStrength = asu;
Log.d(MyTracksConstants.TAG, "Signal Strength: " + signalStrength);
}
@Override
public void run(TrackRecordingService service) {
Log.d(MyTracksConstants.TAG, "Adding signal marker");
Location location = service.getLastLocation();
if (manager == null) {
Log.d(MyTracksConstants.TAG, "Adding signal marker: marker null");
return;
}
if (location == null) {
Log.d(MyTracksConstants.TAG, "Adding signal marker: location null");
return;
}
Waypoint wpt = new Waypoint();
wpt.setName("Signal Strength");
wpt.setType(Waypoint.TYPE_WAYPOINT);
wpt.setTrackId(service.getRecordingTrackId());
int networkType = manager.getNetworkType();
wpt.setIcon(getIcon(networkType));
wpt.setLocation(location);
wpt.setDescription(getDescription());
long waypointId = service.insertWaypointMarker(wpt);
if (waypointId >= 0) {
Log.d(MyTracksConstants.TAG, "Added signal marker");
} else {
Log.e(MyTracksConstants.TAG, "Cannot insert waypoint marker?");
}
}
/**
* Gets a human readable description for the network type.
*
* @param type The integer constant for the network type
* @return A human readable description of the network type
*/
protected String getTypeAsString(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_EDGE:
return "EDGE";
case TelephonyManager.NETWORK_TYPE_GPRS:
return "GPRS";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "UMTS";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "UNKNOWN";
}
}
/**
* Builds a description for the current signal strength.
*
* @return A human readable description of the network state
*/
private String getDescription() {
StringBuffer sb = new StringBuffer();
sb.append(getStrengthAsString());
sb.append("Network Type: ");
sb.append(getTypeAsString(manager.getNetworkType()));
sb.append('\n');
sb.append("Operator: ");
sb.append(manager.getNetworkOperatorName());
sb.append(" / ");
sb.append(manager.getNetworkOperator());
sb.append('\n');
sb.append("Roaming: ");
sb.append(manager.isNetworkRoaming());
sb.append('\n');
List<NeighboringCellInfo> infos = manager.getNeighboringCellInfo();
Log.i(MyTracksConstants.TAG, "Found " + infos.size() + " cells.");
if (infos.size() > 0) {
sb.append("Neighbors: ");
for (NeighboringCellInfo info : infos) {
sb.append(info.toString());
sb.append(' ');
}
sb.append('\n');
}
CellLocation cell = manager.getCellLocation();
if (cell != null) {
sb.append("Cell: ");
sb.append(cell.toString());
sb.append('\n');
}
return sb.toString();
}
/**
* Gets the url for the waypoint icon for the current network type.
*
* @param type The network type
* @return A url to a image to use as the waypoint icon
*/
protected String getIcon(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
return "http://maps.google.com/mapfiles/ms/micons/green.png";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "http://maps.google.com/mapfiles/ms/micons/red.png";
}
}
public void shutdown() {
if (manager != null) {
manager.listen(this, PhoneStateListener.LISTEN_NONE);
manager = null;
}
}
public String getStrengthAsString() {
return "Strength: " + signalStrength + "\n";
}
protected Context getContext() {
return context;
}
}
@@ -0,0 +1,155 @@
/*
* 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.MyTracksConstants;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.telephony.PhoneStateListener;
import android.telephony.SignalStrength;
import android.telephony.TelephonyManager;
import android.util.Log;
/**
* A class to monitor the network signal strength.
*
* @author Sandor Dornbush
*/
public class SignalStrengthTaskModern extends SignalStrengthTask {
private SignalStrength signalStrength = null;
public SignalStrengthTaskModern(Context c) {
super(c);
}
@Override
protected int getListenEvents() {
return PhoneStateListener.LISTEN_SIGNAL_STRENGTHS;
}
/**
* Gets a human readable description for the network type.
*
* @param type The integer constant for the network type
* @return A human readable description of the network type
*/
@Override
protected String getTypeAsString(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_1xRTT:
return "1xRTT";
case TelephonyManager.NETWORK_TYPE_CDMA:
return "CDMA";
case TelephonyManager.NETWORK_TYPE_EDGE:
return "EDGE";
case TelephonyManager.NETWORK_TYPE_EVDO_0:
return "EVDO 0";
case TelephonyManager.NETWORK_TYPE_EVDO_A:
return "EVDO A";
case TelephonyManager.NETWORK_TYPE_GPRS:
return "GPRS";
case TelephonyManager.NETWORK_TYPE_HSDPA:
return "HSDPA";
case TelephonyManager.NETWORK_TYPE_HSPA:
return "HSPA";
case TelephonyManager.NETWORK_TYPE_HSUPA:
return "HSUPA";
case TelephonyManager.NETWORK_TYPE_UMTS:
return "UMTS";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "UNKNOWN";
}
}
/**
* Gets the url for the waypoint icon for the current network type.
*
* @param type The network type
* @return A url to a image to use as the waypoint icon
*/
@Override
protected String getIcon(int type) {
switch (type) {
case TelephonyManager.NETWORK_TYPE_1xRTT:
case TelephonyManager.NETWORK_TYPE_CDMA:
case TelephonyManager.NETWORK_TYPE_GPRS:
case TelephonyManager.NETWORK_TYPE_EDGE:
return "http://maps.google.com/mapfiles/ms/micons/green.png";
case TelephonyManager.NETWORK_TYPE_EVDO_0:
case TelephonyManager.NETWORK_TYPE_EVDO_A:
case TelephonyManager.NETWORK_TYPE_HSDPA:
case TelephonyManager.NETWORK_TYPE_HSPA:
case TelephonyManager.NETWORK_TYPE_HSUPA:
case TelephonyManager.NETWORK_TYPE_UMTS:
return "http://maps.google.com/mapfiles/ms/micons/blue.png";
case TelephonyManager.NETWORK_TYPE_UNKNOWN:
default:
return "http://maps.google.com/mapfiles/ms/micons/red.png";
}
}
@Override
public String getStrengthAsString() {
if (signalStrength == null) {
return "Strength: " + getContext().getString(R.string.unknown) + "\n";
}
StringBuffer sb = new StringBuffer();
if (signalStrength.isGsm()) {
appendSignal(signalStrength.getGsmSignalStrength(),
R.string.gsm_strength,
sb);
maybeAppendSignal(signalStrength.getGsmBitErrorRate(),
R.string.error_rate,
sb);
} else {
appendSignal(signalStrength.getCdmaDbm(), R.string.cdma_strength, sb);
appendSignal(signalStrength.getCdmaEcio() / 10.0, R.string.ecio, sb);
appendSignal(signalStrength.getEvdoDbm(), R.string.evdo_strength, sb);
appendSignal(signalStrength.getEvdoEcio() / 10.0, R.string.ecio, sb);
appendSignal(signalStrength.getEvdoSnr(),
R.string.signal_to_noise_ratio,
sb);
}
return sb.toString();
}
private void maybeAppendSignal(
int signal, int signalFormat, StringBuffer sb) {
if (signal > 0) {
sb.append(getContext().getString(signalFormat, signal));
}
}
private void appendSignal(int signal, int signalFormat, StringBuffer sb) {
sb.append(getContext().getString(signalFormat, signal));
sb.append("\n");
}
private void appendSignal(double signal, int signalFormat, StringBuffer sb) {
sb.append(getContext().getString(signalFormat, signal));
sb.append("\n");
}
@Override
public void onSignalStrengthsChanged(SignalStrength signalStrength) {
Log.d(MyTracksConstants.TAG, "Signal Strength Modern: " + signalStrength);
this.signalStrength = signalStrength;
}
}
@@ -0,0 +1,146 @@
/*
* 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.MyTracksConstants;
import com.google.android.apps.mytracks.util.UnitConversions;
import android.util.Log;
/**
* This class manages inserting time or distance splits.
*
* @author Sandor Dornbush
*/
public class SplitManager {
/**
* The frequency of splits.
*/
private int splitFrequency = 0;
/**
* The next distance when a split should be taken.
*/
private double nextSplitDistance = 0;
/**
* Splits executer.
*/
private PeriodicTaskExecuter splitExecuter = null;
private boolean metricUnits;
private final TrackRecordingService service;
public SplitManager(TrackRecordingService service) {
this.service = service;
}
/**
* Restores the manager.
*/
public void restore() {
if ((splitFrequency > 0) && (splitExecuter != null)) {
splitExecuter.scheduleTask(splitFrequency * 60000);
}
calculateNextSplit();
}
/**
* Shuts down the manager.
*/
public void shutdown() {
if (splitExecuter != null) {
splitExecuter.shutdown();
}
}
/**
* Calculates the next distance that a split should be inserted at.
*/
public void calculateNextSplit() {
if (splitFrequency >= 0) {
nextSplitDistance = Double.MAX_VALUE;
Log.d(MyTracksConstants.TAG,
"SplitManager: Distance splits dissabled.");
return;
}
double dist = service.getTripStatistics().getTotalDistance() / 1000;
if (!metricUnits) {
dist *= UnitConversions.KM_TO_MI;
}
// The index will be negative since the frequency is negative.
int index = (int) (dist / splitFrequency);
index -= 1;
nextSplitDistance = splitFrequency * index;
Log.d(MyTracksConstants.TAG,
"SplitManager: Next split distance: " + nextSplitDistance);
}
/**
* Updates split manager with new trip statistics.
*/
public void updateSplits() {
if (this.splitFrequency >= 0) {
return;
}
// Convert the distance in meters to km or mi.
double distance = service.getTripStatistics().getTotalDistance() / 1000.0;
if (!metricUnits) {
distance *= UnitConversions.KM_TO_MI;
}
if (distance > this.nextSplitDistance) {
service.insertStatisticsMarker(service.getLastLocation());
calculateNextSplit();
}
}
/**
* Sets the split frequency.
* &lt; 0 Use the absolute value as a distance in the current measurement km
* or mi
* 0 Turn off splits
* &gt; 0 Use the value as a time in minutes
* @param splitFrequency The frequency in time or distance
*/
public void setSplitFrequency(int splitFrequency) {
this.splitFrequency = splitFrequency;
if (splitFrequency < 1) {
if (splitExecuter != null) {
splitExecuter.shutdown();
splitExecuter = null;
}
}
if (splitFrequency > 0) {
if (splitExecuter == null) {
TimeSplitTask splitter = new TimeSplitTask();
splitExecuter = new PeriodicTaskExecuter(splitter, service);
}
splitExecuter.scheduleTask(splitFrequency * 60000);
} else {
// For distance based splits.
calculateNextSplit();
}
}
public void setMetricUnits(boolean metricUnits) {
this.metricUnits = metricUnits;
calculateNextSplit();
}
}
@@ -0,0 +1,182 @@
/*
* 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;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.content.Context;
import android.content.SharedPreferences;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import java.util.Locale;
/**
* This class will periodically announce the user's trip statitics.
*
* @author Sandor Dornbush
*/
public class StatusAnnouncerTask
implements TextToSpeech.OnInitListener, PeriodicTask {
/**
* The rate at which announcements are spoken.
*/
private static final float TTS_SPEECH_RATE = 0.9f;
/**
* A pointer to the service context.
*/
private final Context context;
/**
* String utilities.
*/
private final StringUtils stringUtils;
/**
* The interface to the text to speech engine.
*/
private TextToSpeech tts = null;
/**
* The response recieved from the TTS engine ater initialization.
*/
private int ttsStatus = TextToSpeech.ERROR;
/**
* Constructs the announcer and start the TTS engine.
*/
public StatusAnnouncerTask(Context context) {
this.context = context;
this.stringUtils = new StringUtils(context);
tts = new TextToSpeech(context, this);
// Force the language to be the same as the string we will be speaking.
tts.setLanguage(Locale.getDefault());
// Slow down the speed just a bit as it is hard to hear when exercising.
tts.setSpeechRate(TTS_SPEECH_RATE);
}
/**
* Notifies that the tts engine is done with initialization.
*/
public void onInit(int status) {
this.ttsStatus = status;
Log.i(MyTracksConstants.TAG, "TrackRecordingService.TTS init: " + status);
}
/**
* Announces the trip status.
*/
@Override
public void run(TrackRecordingService service) {
if (ttsStatus != TextToSpeech.SUCCESS && tts != null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer Tts not initialized.");
return;
}
if (service == null || service.getTripStatistics() == null) {
Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized.");
return;
}
String announcement = getAnnouncement(service.getTripStatistics());
Log.d(MyTracksConstants.TAG, "Announcement: " + announcement);
tts.speak(announcement, TextToSpeech.QUEUE_FLUSH, null);
}
/**
* Builds the announcement string.
*
* @return The string that will be read to the user
*/
private String getAnnouncement(final TripStatistics stats) {
SharedPreferences preferences =
context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
boolean metricUnits = true;
boolean reportSpeed = true;
if (preferences != null) {
metricUnits =
preferences.getBoolean(context.getString(R.string.metric_units_key),
true);
reportSpeed =
preferences.getBoolean(context.getString(R.string.report_speed_key),
true);
}
double d = stats.getTotalDistance() / 1000;
double s = stats.getAverageMovingSpeed() * 3.6;
if (d == 0) {
return context.getString(R.string.announce_no_distance);
}
int speedLabel;
if (metricUnits) {
if (reportSpeed) {
speedLabel = R.string.kilometer_per_hour_long;
} else {
speedLabel = R.string.per_kilometer;
}
} else {
s *= UnitConversions.KMH_TO_MPH;
d *= UnitConversions.KM_TO_MI;
if (reportSpeed) {
speedLabel = R.string.mile_per_hour_long;
} else {
speedLabel = R.string.per_mile;
}
}
String speed = null;
if ((s == 0) || Double.isNaN(s)) {
speed = context.getString(R.string.unknown);
} else {
if (reportSpeed) {
speed = String.format("%.1f", s);
} else {
double pace = 3600000.0 / s;
Log.w(MyTracksConstants.TAG,
"Converted speed: " + s + " to pace: " + pace);
speed = stringUtils.formatTimeLong((long) pace);
}
}
return context.getString(R.string.announce_template,
context.getString(R.string.total_distance_label),
d,
context.getString(metricUnits
? R.string.kilometers_long
: R.string.miles_long),
stringUtils.formatTimeLong(stats.getMovingTime()),
speed,
context.getString(speedLabel));
}
public void shutdown() {
tts.shutdown();
}
@Override
public void start() {
}
}
@@ -0,0 +1,80 @@
/*
* 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 android.util.Log;
import com.google.android.apps.mytracks.MyTracksConstants;
/**
* This class will manage a period task executer.
*
* @author Sandor Dornbush
*/
public class TaskExecuterManager {
int frequency;
PeriodicTask task;
PeriodicTaskExecuter executer;
public TaskExecuterManager(int frequency,
PeriodicTask task,
TrackRecordingService service) {
this.task = task;
setFrequency(frequency, service);
}
public int getFrequency() {
return frequency;
}
/**
* Sets the frequency that the task should be run at.
* If needed the task will be scheduled.
*
* @param frequency The frequency in minutes for the task to run
* @param service The service to run the task on
*/
public void setFrequency(int frequency, TrackRecordingService service) {
this.frequency = frequency;
Log.i(MyTracksConstants.TAG, "Frequency set to: " + frequency);
if (frequency == -1) {
if (executer != null) {
executer.shutdown();
executer = null;
Log.i(MyTracksConstants.TAG,
"Shut down service: " + task.getClass().getSimpleName());
}
} else {
if (executer == null) {
task.start();
executer = new PeriodicTaskExecuter(task, service);
}
executer.scheduleTask(frequency * 60000);
}
}
/**
* Restore the task at the current frequency.
*/
public void restore() {
if (frequency > 0) {
task.start();
executer.scheduleTask(frequency * 60000);
}
}
}
@@ -0,0 +1,37 @@
/*
* 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;
/**
* A simple task to insert statistics markers every n minutes.
* @author Sandor Dornbush
*/
public class TimeSplitTask implements PeriodicTask {
@Override
public void run(TrackRecordingService service) {
service.insertStatisticsMarker(service.getLastLocation());
}
@Override
public void shutdown() {
}
@Override
public void start() {
}
}
@@ -0,0 +1,986 @@
/*
* Copyright 2008 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.MyTracks;
import com.google.android.apps.mytracks.MyTracksConstants;
import com.google.android.apps.mytracks.MyTracksSettings;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.MyTracksUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.util.Log;
import java.util.Timer;
import java.util.TimerTask;
/**
* A background service that registers a location listener and records track
* points. Track points are saved to the MyTracksProvider.
*
* @author Leif Hendrik Wilden
*/
public class TrackRecordingService extends Service implements LocationListener {
private static final String STATISTICS_ICON_URL =
"http://maps.google.com/mapfiles/ms/micons/ylw-pushpin.png";
private NotificationManager notificationManager;
private LocationManager locationManager;
private WakeLock wakeLock;
private int minRecordingInterval =
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL;
private int minRecordingDistance =
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE;
private int maxRecordingDistance =
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE;
private int minRequiredAccuracy =
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY;
private long recordingTrackId = -1;
private long currentWaypointId = -1;
/**
* For debugging. Keep track of calls to onCreate().
*/
private boolean onCreateWasCalled = false;
/** The timer posts a runnable to the main thread via this handler. */
private final Handler handler = new Handler();
/**
* Utilities to deal with the database.
*/
private MyTracksProviderUtils providerUtils;
private TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder();
private TripStatisticsBuilder waypointStatsBuilder = new TripStatisticsBuilder();
/**
* Current length of the recorded track. This length is calculated from the
* recorded points (as compared to each location fix). It's used to overlay
* waypoints precisely in the elevation profile chart.
*/
private double length = 0;
/**
* Status announcer executer.
*/
private PeriodicTaskExecuter executer;
private TaskExecuterManager signalManager;
private SplitManager splitManager;
/**
* The interval in milliseconds that we have requested to be notified of gps
* readings.
*/
private long currentRecordingInterval = 0;
/**
* The policy used to decide how often we should request gps updates.
*/
private LocationListenerPolicy locationListenerPolicy =
new AbsoluteLocationListenerPolicy(0);
/**
* Task invoked by a timer periodically to make sure the location listener is
* still registered.
*/
private final TimerTask checkLocationListener = new TimerTask() {
@Override
public void run() {
if (!onCreateWasCalled) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService is running, but onCreate not called.");
}
if (isRecording) {
handler.post(new Runnable() {
public void run() {
Log.d(MyTracksConstants.TAG,
"Re-registering location listener with TrackRecordingService.");
unregisterLocationListener();
registerLocationListener();
}
});
} else {
Log.w(MyTracksConstants.TAG,
"Track recording service is paused. That should not be.");
}
}
};
/**
* This timer invokes periodically the checkLocationListener timer task.
*/
private final Timer timer = new Timer();
/**
* Is the phone currently moving?
*/
private boolean isMoving = true;
/**
* Is the service currently recording a track?
*/
private boolean isRecording = false;
/**
* Last good location the service has received from the location listener
*/
private Location lastLocation = null;
/**
* Last valid location (i.e. not a marker) that was recorded.
*/
private Location lastValidLocation = null;
/**
* The frequency of status announcements.
*/
private int announcementFrequency = -1;
private static boolean mTTSAvailable;
/* establish whether the "new" class is available to us */
static {
try {
SafeStatusAnnouncerTask.checkAvailable();
mTTSAvailable = true;
} catch (Throwable t) {
mTTSAvailable = false;
}
}
/*
* Utility functions
*/
/**
* Inserts a new location in the track points db and updates the corresponding
* track in the track db.
*
* @param recordingTrack the track that is currently being recorded
* @param location the location to be inserted
* @param lastRecordedLocation the last recorded location before this one (or
* null if none)
* @param lastRecordedLocationId the id of the last recorded location (or -1
* if none)
* @param trackId the id of the track
* @return true if successful. False if SQLite3 threw an exception.
*/
private boolean insertLocation(Track recordingTrack, Location location,
Location lastRecordedLocation, long lastRecordedLocationId,
long trackId) {
// Keep track of length along recorded track (needed when a waypoint is
// inserted):
if (MyTracksUtils.isValidLocation(location)) {
if (lastValidLocation != null) {
length += location.distanceTo(lastValidLocation);
}
lastValidLocation = location;
}
// Insert the new location:
try {
Uri pointUri = providerUtils.insertTrackPoint(location, trackId);
int pointId = Integer.parseInt(pointUri.getLastPathSegment());
// Update the current track:
if (lastRecordedLocation != null
&& lastRecordedLocation.getLatitude() < 90) {
ContentValues values = new ContentValues();
TripStatistics stats = statsBuilder.getStatistics();
if (recordingTrack.getStartId() < 0) {
values.put(TracksColumns.STARTID, pointId);
recordingTrack.setStartId(pointId);
}
values.put(TracksColumns.STOPID, pointId);
values.put(TracksColumns.STOPTIME, System.currentTimeMillis());
values.put(TracksColumns.NUMPOINTS,
recordingTrack.getNumberOfPoints() + 1);
values.put(TracksColumns.MINLAT, stats.getBottom());
values.put(TracksColumns.MAXLAT, stats.getTop());
values.put(TracksColumns.MINLON, stats.getLeft());
values.put(TracksColumns.MAXLON, stats.getRight());
values.put(TracksColumns.TOTALDISTANCE, stats.getTotalDistance());
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
values.put(TracksColumns.MOVINGTIME, stats.getMovingTime());
values.put(TracksColumns.AVGSPEED, stats.getAverageSpeed());
values.put(TracksColumns.AVGMOVINGSPEED, stats.getAverageMovingSpeed());
values.put(TracksColumns.MAXSPEED, stats.getMaxSpeed());
values.put(TracksColumns.MINELEVATION, stats.getMinElevation());
values.put(TracksColumns.MAXELEVATION, stats.getMaxElevation());
values.put(TracksColumns.ELEVATIONGAIN, stats.getTotalElevationGain());
values.put(TracksColumns.MINGRADE, stats.getMinGrade());
values.put(TracksColumns.MAXGRADE, stats.getMaxGrade());
getContentResolver().update(TracksColumns.CONTENT_URI,
values, "_id=" + recordingTrack.getId(), null);
updateCurrentWaypoint();
}
} catch (SQLiteException e) {
// Insert failed, most likely because of SqlLite error code 5
// (SQLite_BUSY). This is expected to happen extremely rarely (if our
// listener gets invoked twice at about the same time).
Log.w(MyTracksConstants.TAG,
"Caught SQLiteException: " + e.getMessage(), e);
return false;
}
splitManager.updateSplits();
return true;
}
private void updateCurrentWaypoint() {
if (currentWaypointId >= 0) {
ContentValues values = new ContentValues();
TripStatistics waypointStats = waypointStatsBuilder.getStatistics();
values.put(WaypointsColumns.STARTTIME, waypointStats.getStartTime());
values.put(WaypointsColumns.LENGTH, length);
values.put(WaypointsColumns.DURATION, System.currentTimeMillis()
- statsBuilder.getStatistics().getStartTime());
values.put(WaypointsColumns.TOTALDISTANCE,
waypointStats.getTotalDistance());
values.put(WaypointsColumns.TOTALTIME, waypointStats.getTotalTime());
values.put(WaypointsColumns.MOVINGTIME, waypointStats.getMovingTime());
values.put(WaypointsColumns.AVGSPEED, waypointStats.getAverageSpeed());
values.put(WaypointsColumns.AVGMOVINGSPEED,
waypointStats.getAverageMovingSpeed());
values.put(WaypointsColumns.MAXSPEED, waypointStats.getMaxSpeed());
values.put(WaypointsColumns.MINELEVATION,
waypointStats.getMinElevation());
values.put(WaypointsColumns.MAXELEVATION,
waypointStats.getMaxElevation());
values.put(WaypointsColumns.ELEVATIONGAIN,
waypointStats.getTotalElevationGain());
values.put(WaypointsColumns.MINGRADE, waypointStats.getMinGrade());
values.put(WaypointsColumns.MAXGRADE, waypointStats.getMaxGrade());
getContentResolver().update(WaypointsColumns.CONTENT_URI,
values, "_id=" + currentWaypointId, null);
}
}
/**
* Tries to acquire a partial wake lock if not already acquired. Logs errors
* and gives up trying in case the wake lock cannot be acquired.
*/
public void acquireWakeLock() {
try {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
if (pm == null) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Power manager not found!");
return;
}
if (wakeLock == null) {
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
MyTracksConstants.TAG);
if (wakeLock == null) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Could not create wake lock (null).");
return;
}
}
if (!wakeLock.isHeld()) {
wakeLock.acquire();
if (!wakeLock.isHeld()) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Could not acquire wake lock.");
}
}
} catch (RuntimeException e) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Caught unexpected exception: "
+ e.getMessage(), e);
}
}
/**
* Shows the notification message and icon in the notification bar.
*/
public void showNotification() {
if (isRecording) {
Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */,
System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(
this, 0 /* requestCode */, new Intent(this, MyTracks.class),
0 /* flags */);
notification.setLatestEventInfo(this, getString(R.string.app_name),
getString(R.string.recording_your_track), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
notificationManager.notify(1, notification);
} else {
notificationManager.cancelAll();
}
}
public void registerLocationListener() {
if (locationManager == null) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Do not have any location manager.");
return;
}
Log.d(MyTracksConstants.TAG,
"Preparing to register location listener w/ TrackRecordingService...");
try {
long desiredInterval = locationListenerPolicy.getDesiredPollingInterval();
locationManager.requestLocationUpdates(
MyTracksConstants.GPS_PROVIDER, desiredInterval,
locationListenerPolicy.getMinDistance(),
// , 0 /* minDistance, get all updates to properly time pauses */
TrackRecordingService.this);
currentRecordingInterval = desiredInterval;
Log.d(MyTracksConstants.TAG,
"...location listener now registered w/ TrackRecordingService @ "
+ currentRecordingInterval);
} catch (RuntimeException e) {
Log.e(MyTracksConstants.TAG,
"Could not register location listener: " + e.getMessage(), e);
}
}
public void unregisterLocationListener() {
if (locationManager == null) {
Log.e(MyTracksConstants.TAG,
"TrackRecordingService: Do not have any location manager.");
return;
}
locationManager.removeUpdates(this);
Log.d(MyTracksConstants.TAG,
"Location listener now unregistered w/ TrackRecordingService.");
}
private void restoreStats() {
if (recordingTrackId < 0) {
return;
}
Track track = providerUtils.getTrack(recordingTrackId);
if (track == null) {
return;
}
TripStatistics stats = track.getStatistics();
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(stats.getStartTime());
if (executer != null) {
executer.scheduleTask(announcementFrequency * 60000);
}
signalManager.restore();
splitManager.restore();
length = 0;
lastValidLocation = null;
Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId);
if (waypoint != null) {
currentWaypointId = waypoint.getId();
waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getStatistics());
} else {
// This should never happen, but we got to do something so life goes on:
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(stats.getStartTime());
currentWaypointId = -1;
}
Cursor cursor = null;
try {
cursor = providerUtils.getLocationsCursor(
recordingTrackId, -1, MyTracksConstants.MAX_LOADED_TRACK_POINTS,
true);
if (cursor != null) {
if (cursor.moveToLast()) {
do {
Location location = providerUtils.createLocation(cursor);
if (MyTracksUtils.isValidLocation(location)) {
statsBuilder.addLocation(location, location.getTime());
if (lastValidLocation != null) {
length += location.distanceTo(lastValidLocation);
}
lastValidLocation = location;
}
} while (cursor.moveToPrevious());
}
statsBuilder.getStatistics().setMovingTime(stats.getMovingTime());
statsBuilder.pauseAt(stats.getStopTime());
statsBuilder.resume();
} else {
Log.e(MyTracksConstants.TAG, "Could not get track points cursor.");
}
} catch (RuntimeException e) {
Log.e(MyTracksConstants.TAG, "Error while restoring track.", e);
} finally {
if (cursor != null) {
cursor.close();
}
}
splitManager.calculateNextSplit();
}
/*
* Location listener implementation: =================================
*/
@Override
public void onLocationChanged(Location location) {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onLocationChanged");
try {
// Don't record if the service has been asked to pause recording:
if (!isRecording) {
Log.w(MyTracksConstants.TAG,
"Not recording because recording has been paused.");
return;
}
// This should never happen, but just in case (we really don't want the
// service to crash):
if (location == null) {
Log.w(MyTracksConstants.TAG,
"Location changed, but location is null.");
return;
}
// Don't record if the accuracy is too bad:
if (location.getAccuracy() > minRequiredAccuracy) {
Log.d(MyTracksConstants.TAG,
"Not recording. Bad accuracy.");
return;
}
// At least one track must be available for appending points:
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack == null) {
Log.d(MyTracksConstants.TAG,
"Not recording. No track to append to available.");
return;
}
if (MyTracksUtils.isValidLocation(location)) {
long now = System.currentTimeMillis();
statsBuilder.addLocation(location, now);
waypointStatsBuilder.addLocation(location, now);
}
// Update the idle time if needed.
locationListenerPolicy.updateIdleTime(statsBuilder.getIdleTime());
if (currentRecordingInterval
!= locationListenerPolicy.getDesiredPollingInterval()) {
registerLocationListener();
}
Location lastRecordedLocation = providerUtils.getLastLocation();
long lastRecordedLocationId =
providerUtils.getLastLocationId(recordingTrackId);
double distanceToLastRecorded = Double.POSITIVE_INFINITY;
if (lastRecordedLocation != null) {
distanceToLastRecorded = location.distanceTo(lastRecordedLocation);
}
double distanceToLast = Double.POSITIVE_INFINITY;
if (lastLocation != null) {
distanceToLast = location.distanceTo(lastLocation);
}
// If the user has been stationary for two recording just record the first
// two and ignore the rest. This code will only have an effect if the
// maxRecordingDistance = 0
if (distanceToLast == 0) {
if (isMoving) {
Log.d(MyTracksConstants.TAG, "Found two identical locations.");
isMoving = false;
if (lastLocation != null && lastRecordedLocation != null
&& !lastRecordedLocation.equals(lastLocation)) {
// Need to write the last location. This will happen when
// lastRecordedLocation.distance(lastLocation) <
// minRecordingDistance
if (!insertLocation(recordingTrack, lastLocation,
lastRecordedLocation, lastRecordedLocationId,
recordingTrackId)) {
return;
}
lastRecordedLocationId++;
}
} else {
Log.d(MyTracksConstants.TAG,
"Not recording. More than two identical locations.");
}
} else if (distanceToLastRecorded > minRecordingDistance) {
if (lastLocation != null && !isMoving) {
// Last location was the last stationary location. Need to go back and
// add it.
if (!insertLocation(recordingTrack, lastLocation,
lastRecordedLocation, lastRecordedLocationId, recordingTrackId)) {
return;
}
lastRecordedLocationId++;
isMoving = true;
}
// If separation from last recorded point is too large insert a
// separator to indicate end of a segment:
boolean startNewSegment =
lastRecordedLocation != null
&& lastRecordedLocation.getLatitude() < 90
&& distanceToLastRecorded > maxRecordingDistance
&& recordingTrack.getStartId() >= 0;
if (startNewSegment) {
// Insert a separator point to indicate start of new track:
Log.d(MyTracksConstants.TAG, "Inserting a separator.");
Location separator = new Location(MyTracksConstants.GPS_PROVIDER);
separator.setLongitude(0);
separator.setLatitude(100);
separator.setTime(lastRecordedLocation.getTime());
providerUtils.insertTrackPoint(separator, recordingTrackId);
}
if (!insertLocation(recordingTrack, location, lastRecordedLocation,
lastRecordedLocationId, recordingTrackId)) {
return;
}
} else {
Log.d(MyTracksConstants.TAG, String.format(
"Not recording. Distance to last recorded point (%f m) is less than"
+ " %d m.", distanceToLastRecorded, minRecordingDistance));
}
} catch (Error e) {
// Probably important enough to rethrow.
Log.e(MyTracksConstants.TAG, "Error in onLocationChanged", e);
throw e;
} catch (RuntimeException e) {
// Safe usually to trap exceptions.
Log.e(MyTracksConstants.TAG,
"Trapping exception in onLocationChanged", e);
throw e;
}
lastLocation = location;
}
@Override
public void onProviderDisabled(String provider) {
// Do nothing
}
@Override
public void onProviderEnabled(String provider) {
// Do nothing
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
// Do nothing
}
/*
* SharedPreferencesChangeListener interface implementation. Note that
* services don't currently receive this event (Android platform limitation).
* This should be called from an activity whenever settings change.
*/
/**
* 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)
*/
public void onSharedPreferenceChanged(String key) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService.onSharedPreferenceChanged");
SharedPreferences sharedPreferences =
getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0);
if (sharedPreferences == null) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService: Couldn't get shared preferences.");
return;
}
if (key == null
|| key.equals(getString(R.string.min_recording_distance_key))) {
minRecordingDistance = sharedPreferences.getInt(
getString(R.string.min_recording_distance_key),
MyTracksSettings.DEFAULT_MIN_RECORDING_DISTANCE);
Log.d(MyTracksConstants.TAG,
"TrackRecordingService: minRecordingDistance = "
+ minRecordingDistance);
}
if (key == null
|| key.equals(getString(R.string.max_recording_distance_key))) {
maxRecordingDistance = sharedPreferences.getInt(
getString(R.string.max_recording_distance_key),
MyTracksSettings.DEFAULT_MAX_RECORDING_DISTANCE);
}
if (key == null
|| key.equals(getString(R.string.min_recording_interval_key))) {
minRecordingInterval = sharedPreferences.getInt(
getString(R.string.min_recording_interval_key),
MyTracksSettings.DEFAULT_MIN_RECORDING_INTERVAL);
switch (minRecordingInterval) {
case -2:
// Battery Miser
// min: 30 seconds
// max: 5 minutes
// minDist: 5 meters Choose battery life over moving time accuracy.
locationListenerPolicy =
new AdaptiveLocationListenerPolicy(30000, 300000, 5);
break;
case -1:
// High Accuracy
// min: 1 second
// max: 30 seconds
// minDist: 0 meters get all updates to properly measure moving time.
locationListenerPolicy =
new AdaptiveLocationListenerPolicy(1000, 30000, 0);
break;
default:
locationListenerPolicy =
new AbsoluteLocationListenerPolicy(minRecordingInterval * 1000);
}
}
if (key == null
|| key.equals(getString(R.string.min_required_accuracy_key))) {
minRequiredAccuracy = sharedPreferences.getInt(
getString(R.string.min_required_accuracy_key),
MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY);
}
if (key == null
|| key.equals(getString(R.string.announcement_frequency_key))) {
announcementFrequency =
sharedPreferences.getInt(getString(R.string.announcement_frequency_key),
-1);
if (mTTSAvailable) {
if (announcementFrequency == -1) {
if (executer != null) {
executer.shutdown();
executer = null;
}
} else {
if (executer == null) {
SafeStatusAnnouncerTask announcer =
new SafeStatusAnnouncerTask(this);
executer = new PeriodicTaskExecuter(announcer, this);
}
executer.scheduleTask(announcementFrequency * 60000);
}
}
}
if (key == null || key.equals(getString(R.string.split_frequency_key))) {
splitManager.setSplitFrequency(
sharedPreferences.getInt(getString(R.string.split_frequency_key), 0));
}
if (key == null
|| key.equals(getString(R.string.signal_sampling_frequency_key))) {
signalManager.setFrequency(sharedPreferences.getInt(
getString(R.string.signal_sampling_frequency_key), -1), this);
}
if (key == null || key.equals(getString(R.string.metric_units_key))) {
splitManager.setMetricUnits(sharedPreferences.getBoolean(
getString(R.string.metric_units_key), true));
}
if (isRecording) {
registerLocationListener();
}
}
/*
* Application lifetime events: ============================
*/
@Override
public void onCreate() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onCreate");
super.onCreate();
onCreateWasCalled = true;
providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager =
(LocationManager) getSystemService(Context.LOCATION_SERVICE);
splitManager = new SplitManager(this);
try {
signalManager =
new TaskExecuterManager(-1, new SignalStrengthTaskModern(this), this);
Log.d(MyTracksConstants.TAG,
"TrackRecordingService using modern signal strength api.");
} catch (LinkageError e) {
Log.w(MyTracksConstants.TAG,
"TrackRecordingService could not load modern signal strength.", e);
signalManager =
new TaskExecuterManager(-1, new SignalStrengthTask(this), this);
}
onSharedPreferenceChanged(null);
restoreStats();
registerLocationListener();
acquireWakeLock();
/**
* After 5 min, check every minute that location listener still is
* registered and spit out additional debugging info to the logs:
*/
timer.schedule(checkLocationListener, 1000 * 60 * 5, 1000 * 60);
isRecording = true;
if (mTTSAvailable && (announcementFrequency != -1)) {
if (executer == null) {
SafeStatusAnnouncerTask announcer = new SafeStatusAnnouncerTask(this);
executer = new PeriodicTaskExecuter(announcer, this);
}
}
}
@Override
public void onDestroy() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onDestroy");
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
isRecording = false;
showNotification();
unregisterLocationListener();
if (executer != null) {
executer.shutdown();
}
splitManager.shutdown();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onBind");
return binder;
}
@Override
public boolean onUnbind(Intent intent) {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.onUnbind");
return super.onUnbind(intent);
}
@Override
public boolean stopService(Intent name) {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.stopService");
unregisterLocationListener();
return super.stopService(name);
}
public long insertWaypointMarker(Waypoint waypoint) {
if (waypoint.getLocation() != null) {
waypoint.setLength(length);
waypoint.setDuration(waypoint.getLocation().getTime()
- statsBuilder.getStatistics().getStartTime());
Uri uri = providerUtils.insertWaypoint(waypoint);
return Long.parseLong(uri.getLastPathSegment());
}
return -1;
}
/**
* Inserts a statistics marker. A statistics marker holds the stats for the
* last segment up to this marker.
*
* @param location the location where to insert
* @return the unique id of the inserted marker
*/
public long insertStatisticsMarker(Location location) {
StringUtils utils = new StringUtils(TrackRecordingService.this);
// Create a new waypoint to save
Waypoint waypoint = new Waypoint();
// Set stop and total time in the stats data
final long time = System.currentTimeMillis();
waypointStatsBuilder.pauseAt(time);
// Override the duration - it's not the duration from the last waypoint, but
// the duration from the beginning of the whole track
waypoint.setDuration(time - statsBuilder.getStatistics().getStartTime());
// Set the rest of the waypoint data
waypoint.setTrackId(recordingTrackId);
waypoint.setType(Waypoint.TYPE_STATISTICS);
waypoint.setName(TrackRecordingService.this.getString(R.string.statistics));
waypoint.setStatistics(waypointStatsBuilder.getStatistics());
waypoint.setDescription(utils.generateWaypointDescription(waypoint));
waypoint.setLocation(location);
waypoint.setIcon(STATISTICS_ICON_URL);
waypoint.setLength(length);
waypoint.setStartId(providerUtils.getLastLocationId(recordingTrackId));
Uri uri = providerUtils.insertWaypoint(waypoint);
// Create a new stats keeper for the next marker
waypointStatsBuilder = new TripStatisticsBuilder();
waypointStatsBuilder.resumeAt(time);
updateCurrentWaypoint();
return Long.parseLong(uri.getLastPathSegment());
}
/**
* The ITrackRecordingService is defined through IDL.
*/
private final ITrackRecordingService.Stub binder =
new ITrackRecordingService.Stub() {
@Override
public boolean isRecording() {
return isRecording;
}
@Override
public long getRecordingTrackId() {
return recordingTrackId;
}
@Override
public boolean hasRecorded() {
return providerUtils.getLastTrackId() >= 0;
}
@Override
public long startNewTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack");
Track track = new Track();
TripStatistics trackStats = track.getStatistics();
track.setName("new");
long startTime = System.currentTimeMillis();
trackStats.setStartTime(startTime);
track.setStartId(-1);
Uri trackUri = providerUtils.insertTrack(track);
long trackId = Long.parseLong(trackUri.getLastPathSegment());
track.setId(trackId);
track.setName(String.format(getString(R.string.new_track), trackId));
providerUtils.updateTrack(track);
recordingTrackId = trackId;
currentWaypointId = insertStatisticsMarker(null);
isRecording = true;
isMoving = true;
statsBuilder = new TripStatisticsBuilder();
statsBuilder.resumeAt(startTime);
if (announcementFrequency != -1 && executer != null) {
executer.scheduleTask(announcementFrequency * 60000);
}
length = 0;
showNotification();
registerLocationListener();
splitManager.restore();
signalManager.restore();
return trackId;
}
/**
* Insert the given waypoint marker. Users can insert waypoint markers
* to tag locations with a name, description, category etc.
*
* @param waypoint a waypoint
* @return the unique id of the inserted marker
*/
@Override
public long insertWaypointMarker(Waypoint waypoint) {
return TrackRecordingService.this.insertWaypointMarker(waypoint);
}
/**
* Insert a statistics marker. A statistics marker holds the stats for
* the last segment up to this marker.
*
* @param location the location where to insert
* @return the unique id of the inserted marker
*/
@Override
public long insertStatisticsMarker(Location location) {
return TrackRecordingService.this.insertStatisticsMarker(location);
}
@Override
public void endCurrentTrack() {
Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack");
isRecording = false;
Track recordingTrack = providerUtils.getTrack(recordingTrackId);
if (recordingTrack != null) {
TripStatistics stats = recordingTrack.getStatistics();
stats.setStopTime(System.currentTimeMillis());
stats.setTotalTime(stats.getStopTime() - stats.getStartTime());
long lastRecordedLocationId =
providerUtils.getLastLocationId(recordingTrackId);
ContentValues values = new ContentValues();
if (lastRecordedLocationId >= 0
&& recordingTrack.getStopId() >= 0) {
values.put(TracksColumns.STOPID, lastRecordedLocationId);
}
values.put(TracksColumns.STOPTIME, stats.getStopTime());
values.put(TracksColumns.TOTALTIME, stats.getTotalTime());
getContentResolver().update(TracksColumns.CONTENT_URI, values,
"_id=" + recordingTrack.getId(), null);
}
showNotification();
recordingTrackId = -1;
}
@Override
public void deleteAllTracks() {
endCurrentTrack();
providerUtils.deleteAllTracks();
}
@Override
public void recordLocation(Location loc) {
onLocationChanged(loc);
}
@Override
public void sharedPreferenceChanged(String key) {
Log.d(MyTracksConstants.TAG,
"TrackRecordingService.sharedPrefereneChange");
onSharedPreferenceChanged(key);
}
};
TripStatistics getTripStatistics() {
return statsBuilder.getStatistics();
}
Location getLastLocation() {
return lastLocation;
}
long getRecordingTrackId() {
return recordingTrackId;
}
}