diff --git a/.hgignore b/.hgignore index 8372fdde3..7220e4cb0 100644 --- a/.hgignore +++ b/.hgignore @@ -3,3 +3,4 @@ syntax:glob */bin */gen/** .metadata +MyTracksTest/annotations.log diff --git a/MyTracks/.classpath b/MyTracks/.classpath index 39a225fb5..7c5cb8f1e 100644 --- a/MyTracks/.classpath +++ b/MyTracks/.classpath @@ -1,10 +1,10 @@ - - - - - - - - - - + + + + + + + + + + diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index c3a1c4874..3e72ff098 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -1,6 +1,7 @@ + + + 1 分钟 + + + 从不 + 5 分钟 + 10 分钟 + 15 分钟 + 总是 + 未知 diff --git a/MyTracks/res/values-zh-rCN/strings.xml b/MyTracks/res/values-zh-rCN/strings.xml index 00d38bcd3..648550a39 100644 --- a/MyTracks/res/values-zh-rCN/strings.xml +++ b/MyTracks/res/values-zh-rCN/strings.xml @@ -15,8 +15,8 @@ See the License for the specific language governing permissions and limitations under the License. --> - - My Tracks + + 记录轨迹 @@ -32,6 +32,7 @@ limitations under the License. 请选择要查看的轨迹 当前轨迹 上一段轨迹 + 累积统计 此设备上没有位置管理器! @@ -122,6 +123,7 @@ limitations under the License. 保存为 GPX 文件 保存为 KML 文件 保存为 CSV 文件 + 保存为 TCX 文件 发送到 Google My Maps 删除所有轨迹 @@ -135,6 +137,7 @@ limitations under the License. GPX 文件 KML 文件 CSV 文件 + TCX 文件 清除地图 @@ -165,9 +168,10 @@ limitations under the License. 记录 分享 + 你使用的 Android 版本无法支持 + 提示频度 语音提示的最小时间间隔 - 最小记录间距 位置记录的最小间隔距离 @@ -202,7 +206,38 @@ limitations under the License. 切割频度 插入新切割标记所需要达到的最大时间或距离 - 步数 + + 自动恢复追踪等待时间 + 手机重启后自动继续上次轨迹记录的最大间隔时间 + + 节奏 + + 只在 Android 2.0 及以上版本支持 + 感应器类型 + 要连接到的感应器类型 + 感应器ID + 发送到感应器的ID. + ID必须是一个整数值. + 蓝牙感应器 + 要连接到的蓝牙感应器 + + + + + 备份 (测试版) + SD 存储卡备份 + 立即备份 + 将你所有轨迹和设置的完整备份写入到 SD 存储卡 + 立即恢复 + 从 SD 存储卡恢复所有轨迹和设置 + 在记录轨迹时无法处理备份数据. + + + 从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗? + 请选择要恢复的备份文件 + 没有找到备份文件. + 将所有轨迹和设置写入 SD 存储卡中. 这需要一些时间... + 从 SD 存储卡读取轨迹和设置中. 这需要一些时间... GPS 卫星 @@ -241,7 +276,7 @@ limitations under the License. 公开 - Unlisted + 未公布 你还没有保存任何地图. @@ -292,24 +327,32 @@ limitations under the License. 秒 秒 + + - m - km + + 千米 - ft - mi + + 英里 - km/h + 千米每小时 - min/km + 每千米 - mi/h + 英里每小时 - min/mi + 每英里 + + + + + + 移动时间 总时间 @@ -320,7 +363,11 @@ limitations under the License. 统计 总距离 距离 - - + 电源 + 节奏 + 心率 + 最大心率 + 请稍候... @@ -336,20 +383,27 @@ limitations under the License. 技术支持 技术支持: - - 1.0.14 - mytracks@googlegroups.com - mytracks.appspot.com - Copyright 2009 Google Inc. - Dylan Casey - Leif Hendrik Wilden - Rodrigo Damazio Bovendorp - Sandor Dornbush + + + + + + + + + + + + + + + + 发送到... - Google My Maps - Google Docs + + 立即发送 创建新地图 选择已有地图 @@ -364,8 +418,9 @@ limitations under the License. 没有发现存贮设备. 请确认插入了 SD 卡并且你的手机没有将挂载 SD 卡到电脑. 无法写入文件. - 无法在 SD 卡上创建目录. + 无法在 SD 存储卡上创建目录. 轨迹已导出到 SD 卡. + 从 SD 存储卡读取完毕. 检查更新 @@ -403,4 +458,29 @@ limitations under the License. GSM 强度 %1$d CDMA 强度 %1$d dBm EVDO 强度 %1$d dBm + + + 蓝牙 + 蓝牙设置 + 设备关联 + 打开 Android 的蓝牙设置关联设备 + + 感应器选项 + + + + + + + + + 连接到 %s + 蓝牙感应器连接中 ... + 未连接蓝牙感应器 + + + 感应器数据时间 + 电源计量器已启用 + 心率监视器已启用 + diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index 722f8e232..6212f9cf2 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -211,8 +211,8 @@ limitations under the License. Maximum time in minutes after which a previously recording track will not be resumed on phone reboot Timestamp as track name - The default track name will be the date and time at the start of the track - Track names will have numbers which increment for each new track. + The default track name will be the date and time at the start of the track + Track names will have numbers which increment for each new track. Pace diff --git a/MyTracks/res/xml/preferences.xml b/MyTracks/res/xml/preferences.xml index 8de128543..848bab5a2 100644 --- a/MyTracks/res/xml/preferences.xml +++ b/MyTracks/res/xml/preferences.xml @@ -103,8 +103,8 @@ limitations under the License. diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index 62117a82c..1e17d7a3c 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -298,13 +298,13 @@ public class ChartActivity extends Activity implements public void onChange(boolean selfChange) { Log.d(MyTracksConstants.TAG, "ChartActivity: ContentObserver.onChange"); // Check for any new locations and append them to the currently - // recording track: - if (!MyTracks.getInstance().isRecording()) { + // recording track. + if (recordingTrackId < 0) { // No track is being recorded. We should not be here. return; } if (selectedTrackId != recordingTrackId) { - // no track, or one other than the recording track is selected, don't + // No track, or one other than the recording track is selected, don't // bother. return; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java index d2bf16d29..d5ff24834 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartValueSeries.java @@ -25,7 +25,7 @@ import android.graphics.Path; import java.text.DecimalFormat; /** - * This class encasulates meta data about one series of values for a chart. + * This class encapsulates meta data about one series of values for a chart. * * @author Sandor Dornbush */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index 449fe0f2a..8d8437d50 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -26,9 +26,9 @@ import com.google.android.apps.mytracks.io.AuthManagerFactory; import com.google.android.apps.mytracks.io.GpxImporter; import com.google.android.apps.mytracks.io.SendToDocs; import com.google.android.apps.mytracks.io.SendToMyMaps; -import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener; import com.google.android.apps.mytracks.io.TrackWriter; import com.google.android.apps.mytracks.io.TrackWriterFactory; +import com.google.android.apps.mytracks.io.SendToMyMaps.OnSendCompletedListener; import com.google.android.apps.mytracks.io.TrackWriterFactory.TrackFileFormat; import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.StatusAnnouncerFactory; @@ -65,9 +65,9 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.Window; import android.view.View.OnTouchListener; import android.view.ViewGroup.LayoutParams; -import android.view.Window; import android.view.WindowManager.BadTokenException; import android.widget.RelativeLayout; import android.widget.TabHost; @@ -97,9 +97,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, /** * Singleton instance */ - private static MyTracks instance = null; - private ChartActivity chartActivity = null; + private static MyTracks instance; + private ChartActivity chartActivity; + public ChartActivity getChartActivity() { return chartActivity; } @@ -178,6 +179,8 @@ public class MyTracks extends TabActivity implements OnTouchListener, * Utilities to deal with the database. */ private MyTracksProviderUtils providerUtils; + + private SharedPreferences sharedPreferences; /** * The connection to the track recording service. @@ -233,20 +236,35 @@ public class MyTracks extends TabActivity implements OnTouchListener, } /** + * Checks whether we have a track recording session in progress. + * In some cases, when the service has crashed or has been restarted + * by the system, we fall back to the shared preferences. + * * @return true if the activity is bound to the track recording service and - * the service is recording a track. + * the service is recording a track or in case the service is down, + * based on settings from the shared preferences. */ public boolean isRecording() { if (trackRecordingService == null) { - return false; + // Fall back to alternative check method. + return isRecordingBasedOnSharedPreferences(); } try { return trackRecordingService.isRecording(); } catch (RemoteException e) { Log.e(MyTracksConstants.TAG, "MyTracks: Remote exception.", e); - return false; + + // Fall back to alternative check method. + return isRecordingBasedOnSharedPreferences(); } } + + private boolean isRecordingBasedOnSharedPreferences() { + // TrackRecordingServices guarantees that recordingTrackId is set to + // -1 if the track has been stopped. + // TODO: Refresh recordingTrackId. + return recordingTrackId >= 0; + } /* * Application lifetime events: @@ -260,6 +278,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, instance = this; providerUtils = MyTracksProviderUtils.Factory.get(this); menuManager = new MenuManager(this); + sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); // The volume we want to control is the Text-To-Speech volume int volumeStream = @@ -295,14 +314,18 @@ public class MyTracks extends TabActivity implements OnTouchListener, tabHost.addView(layout); layout.setOnTouchListener(this); - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (prefs != null) { + if (sharedPreferences != null) { selectedTrackId = - prefs.getLong(getString(R.string.selected_track_key), -1); - recordingTrackId = - prefs.getLong(getString(R.string.recording_track_key), -1); - prefs.registerOnSharedPreferenceChangeListener(this); + sharedPreferences.getLong(getString(R.string.selected_track_key), -1); + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); + Log.d(MyTracksConstants.TAG, "recordingTrackId: " + recordingTrackId + + ", selectedTrackId: " + selectedTrackId); + if (recordingTrackId > 0) { + Intent startIntent = new Intent(this, TrackRecordingService.class); + startService(startIntent); + } } // This will show the eula until the user accepts or quits the app. @@ -1008,20 +1031,15 @@ public class MyTracks extends TabActivity implements OnTouchListener, shareIntent.putExtra(Intent.EXTRA_SUBJECT, getResources().getText(R.string.share_map_subject).toString()); - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); boolean shareUrlOnly = true; - if (prefs != null) { - shareUrlOnly = - prefs.getBoolean(getString(R.string.share_url_only_key), false); + if (sharedPreferences != null) { + shareUrlOnly = sharedPreferences.getBoolean( + getString(R.string.share_url_only_key), false); } String url = MyMapsConstants.MAPSHOP_BASE_URL + "?msa=0&msid=" + mapId; - String msg = (shareUrlOnly - ? url - : String.format( - getResources().getText(R.string.share_map_body_format).toString(), - url)); + String msg = shareUrlOnly ? url : String.format( + getResources().getText(R.string.share_map_body_format).toString(), url); shareIntent.putExtra(Intent.EXTRA_TEXT, msg); startActivity(Intent.createChooser(shareIntent, getResources().getText(R.string.share_map).toString())); @@ -1235,9 +1253,10 @@ public class MyTracks extends TabActivity implements OnTouchListener, private void startRecordingNewTrack() { try { recordingTrackId = trackRecordingService.startNewTrack(); + // Select the recording track. + setSelectedTrack(recordingTrackId); Toast.makeText(this, getString(R.string.status_now_recording), Toast.LENGTH_SHORT).show(); - setSelectedAndRecordingTrack(recordingTrackId, recordingTrackId); } catch (RemoteException e) { Toast.makeText(this, getString(R.string.error_unable_to_start_recording), @@ -1272,7 +1291,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, } catch (RemoteException e) { Log.e(MyTracksConstants.TAG, "Unable to stop recording.", e); } - setRecordingTrack(-1); Intent intent = new Intent(MyTracks.this, MyTracksDetails.class); intent.putExtra("trackid", recordingTrackId); intent.putExtra("hasCancelButton", false); @@ -1327,9 +1345,7 @@ public class MyTracks extends TabActivity implements OnTouchListener, public void setSelectedTrack(final long trackId) { runOnUiThread(new Runnable() { public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - SharedPreferences.Editor editor = prefs.edit(); + SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putLong(getString(R.string.selected_track_key), trackId); editor.commit(); } @@ -1340,44 +1356,6 @@ public class MyTracks extends TabActivity implements OnTouchListener, return selectedTrackId; } - /** - * Writes the recording track id to the shared preferences. - * Executed on the UI thread. - * - * @param trackId the id of the track - */ - private void setRecordingTrack(final long trackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(getString(R.string.recording_track_key), trackId); - editor.commit(); - } - }); - } - - /** - * Writes the selected and the recording track id to the shared preferences. - * Executed on UI thread. - */ - private void setSelectedAndRecordingTrack(final long theSelectedTrackId, - final long theRecordingTrackId) { - runOnUiThread(new Runnable() { - public void run() { - SharedPreferences prefs = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (prefs != null) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(getString(R.string.selected_track_key), theSelectedTrackId); - editor.putLong(getString(R.string.recording_track_key), theRecordingTrackId); - editor.commit(); - } - } - }); - } - /** * Binds to track recording service if it is running. */ diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java index 32212e145..78d7645d4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracksMap.java @@ -118,7 +118,7 @@ public class MyTracksMap extends MapActivity private double variation; /** - * From the shared preferences: + * From the shared preferences. */ private int minRequiredAccuracy = MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY; @@ -135,8 +135,8 @@ public class MyTracksMap extends MapActivity private Location currentLocation; /** - * A thread with a looper. Post to updateTrackHandler to execute Runnables on - * this thread. + * A thread with a looper. Post to updateTrackHandler to execute + * {@link Runnable}s on this thread. */ private HandlerThread updateTrackThread; @@ -146,6 +146,8 @@ public class MyTracksMap extends MapActivity private Handler updateTrackHandler; private MyTracksProviderUtils providerUtils; + + private SharedPreferences sharedPreferences; /** * A runnable that updates the track from the provider (looking for points @@ -331,12 +333,6 @@ public class MyTracksMap extends MapActivity @Override public void onChange(boolean selfChange) { Log.d(MyTracksConstants.TAG, "MyTracksMap: ContentObserver.onChange"); - // Check for any new locations and append them to the currently - // recording track: - if (!MyTracks.getInstance().isRecording()) { - // No track is being recorded. We should not be here. - return; - } if (!isRecordingSelected()) { // No track, or one other than the recording track is selected, // don't bother. @@ -361,30 +357,25 @@ public class MyTracksMap extends MapActivity } }; - // Read shared preferences and register change listener: - SharedPreferences preferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (preferences != null) { - minRequiredAccuracy = preferences.getInt( - getString(R.string.min_required_accuracy_key), - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - recordingTrackId = - preferences.getLong(getString(R.string.recording_track_key), -1); - long selectedTrackId = - preferences.getLong(getString(R.string.selected_track_key), -1); - if (selectedTrackId >= 0) { - setSelectedTrack(selectedTrackId); - } + // Read shared preferences and register change listener. + sharedPreferences = getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (sharedPreferences != null) { + reloadSharedPreferences(sharedPreferences, null); updateOptionsButton(); - preferences.registerOnSharedPreferenceChangeListener(this); + sharedPreferences.registerOnSharedPreferenceChangeListener(this); } } @Override protected void onDestroy() { Log.d(MyTracksConstants.TAG, "MyTracksMap.onDestroy"); + if (updateTrackThread != null) { - updateTrackThread.quit(); + ApiFeatures.getInstance().getApiPlatformAdapter().stopHandlerThread( + updateTrackThread); + } + if (sharedPreferences != null) { + sharedPreferences.unregisterOnSharedPreferenceChangeListener(this); } super.onDestroy(); @@ -451,6 +442,9 @@ public class MyTracksMap extends MapActivity Log.d(MyTracksConstants.TAG, "MyTracksMap.onResume"); super.onResume(); + // Reload all preferences as they might have changed since last run. + reloadSharedPreferences(sharedPreferences, null); + // Make sure any updates that might have happened are propagated to the // Map overlay: observer.onChange(false); @@ -707,11 +701,14 @@ public class MyTracksMap extends MapActivity * @param trackId a given track id */ public void setSelectedTrack(final long trackId) { + Log.d(MyTracksConstants.TAG, "MyTracksMap.setSelectedTrack: " + + "selectedtTrackId = " + selectedTrackId + ", trackId = " + trackId); + if (selectedTrackId == trackId) { // Selected track did not change, nothing to do. mapOverlay.setTrackDrawingEnabled(isATrackSelected()); - mapView.invalidate(); updateOptionsButton(); + mapView.invalidate(); return; } @@ -786,7 +783,7 @@ public class MyTracksMap extends MapActivity menu.setHeaderTitle(R.string.tracklist_this_track); menu.add(0, MyTracksConstants.MENU_EDIT, 0, R.string.tracklist_edit_track); - if (!MyTracks.getInstance().isRecording() || !isRecordingSelected()) { + if (!isRecordingSelected()) { menu.add(0, MyTracksConstants.MENU_SEND_TO_GOOGLE, 0, R.string.tracklist_send_to_google); SubMenu share = menu.addSubMenu(0, MyTracksConstants.MENU_SHARE, 0, @@ -906,30 +903,13 @@ public class MyTracksMap extends MapActivity @Override public void onSharedPreferenceChanged( final SharedPreferences sharedPreferences, final String key) { + Log.d(MyTracksConstants.TAG, + "MyTracksMap.onSharedPreferenceChanged: " + key); if (key != null) { uiHandler.post(new Runnable() { @Override public void run() { - if (key.equals(getString(R.string.min_required_accuracy_key))) { - minRequiredAccuracy = sharedPreferences.getInt( - getString(R.string.min_required_accuracy_key), - MyTracksSettings.DEFAULT_MIN_REQUIRED_ACCURACY); - } else if (key.equals(getString(R.string.selected_track_key))) { - long selectedTrackId = - sharedPreferences.getLong( - getString(R.string.selected_track_key), - -1); - setSelectedTrack(selectedTrackId); - } else if (key.equals(getString(R.string.recording_track_key))) { - recordingTrackId = - sharedPreferences.getLong( - getString(R.string.recording_track_key), - -1); - if (isATrackSelected()) { - mapOverlay.setShowEndMarker(!isRecordingSelected()); - mapView.postInvalidate(); - } - } + reloadSharedPreferences(sharedPreferences, key); } }); } @@ -1009,6 +989,31 @@ public class MyTracksMap extends MapActivity } }; + private void reloadSharedPreferences(SharedPreferences sharedPreferences, + String key) { + 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.recording_track_key))) { + recordingTrackId = sharedPreferences.getLong( + getString(R.string.recording_track_key), -1); + } + if (key == null || key.equals(getString(R.string.selected_track_key))) { + setSelectedTrack(sharedPreferences.getLong( + getString(R.string.selected_track_key), -1)); + } + + // Show end marker if the track has been selected and is not recording. + // Note: This check must be *after* a call to setSelectedTrack(...) above. + if (isATrackSelected()) { + mapOverlay.setShowEndMarker(!isRecordingSelected()); + mapView.postInvalidate(); + } + } + private void readAllNewTrackPoints() { int numPoints = mapOverlay.getNumLocations(); if (numPoints >= MyTracksConstants.MAX_DISPLAYED_TRACK_POINTS) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 403190a6c..9d68c9c27 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -52,7 +52,7 @@ import android.widget.Toast; public class StatsActivity extends Activity implements OnSharedPreferenceChangeListener { - private final StatsUtilities utils; + private StatsUtilities utils; private UIUpdateThread thread; private ContentObserver observer; @@ -94,7 +94,7 @@ public class StatsActivity extends Activity */ private boolean showCurrentSegment = false; - private final MyTracksProviderUtils providerUtils; + private MyTracksProviderUtils providerUtils; /** * A runnable for posting to the UI thread. Will update the total time field. @@ -133,16 +133,14 @@ public class StatsActivity extends Activity } } - public StatsActivity() { - utils = new StatsUtilities(this); - providerUtils = new MyTracksProviderUtilsImpl(this); - } - /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + utils = new StatsUtilities(this); + providerUtils = new MyTracksProviderUtilsImpl(getContentResolver()); + // The volume we want to control is the Text-To-Speech volume int volumeStream = new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream(); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index 1ea043aff..56c70db13 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -345,7 +345,7 @@ public interface MyTracksProviderUtils { * Creates an instance of {@link MyTracksProviderUtils}. */ protected MyTracksProviderUtils newForContext(Context context) { - return new MyTracksProviderUtilsImpl(context); + return new MyTracksProviderUtilsImpl(context.getContentResolver()); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index ffbd77d9c..1e814f102 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -17,8 +17,8 @@ package com.google.android.apps.mytracks.content; import com.google.android.apps.mytracks.stats.TripStatistics; +import android.content.ContentResolver; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import android.location.Location; import android.net.Uri; @@ -35,10 +35,10 @@ import java.util.List; */ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { - private final Context context; + private final ContentResolver contentResolver; - public MyTracksProviderUtilsImpl(Context context) { - this.context = context; + public MyTracksProviderUtilsImpl(ContentResolver contentResolver) { + this.contentResolver = contentResolver; } /** @@ -448,25 +448,22 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void deleteAllTracks() { - context.getContentResolver().delete(TracksColumns.CONTENT_URI, null, null); - context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI, - null, null); - context.getContentResolver().delete( - WaypointsColumns.CONTENT_URI, null, null); + contentResolver.delete(TracksColumns.CONTENT_URI, null, null); + contentResolver.delete(TrackPointsColumns.CONTENT_URI, null, null); + contentResolver.delete(WaypointsColumns.CONTENT_URI, null, null); } @Override public void deleteTrack(long trackId) { Track track = getTrack(trackId); if (track != null) { - context.getContentResolver().delete(TrackPointsColumns.CONTENT_URI, + contentResolver.delete(TrackPointsColumns.CONTENT_URI, "_id>=" + track.getStartId() + " AND _id<=" + track.getStopId(), null); } - context.getContentResolver().delete(WaypointsColumns.CONTENT_URI, + contentResolver.delete(WaypointsColumns.CONTENT_URI, WaypointsColumns.TRACKID + "=" + trackId, null); - context.getContentResolver().delete( - TracksColumns.CONTENT_URI, "_id=" + trackId, null); + contentResolver.delete(TracksColumns.CONTENT_URI, "_id=" + trackId, null); } @Override @@ -491,7 +488,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { "No statistics marker after the deleted one was found."); } } - context.getContentResolver().delete( + contentResolver.delete( WaypointsColumns.CONTENT_URI, "_id=" + waypointId, null); } @@ -503,7 +500,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { final String sortOrder = WaypointsColumns._ID + " LIMIT 1"; Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, selection, @@ -525,7 +522,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public boolean updateWaypoint(Waypoint waypoint) { try { - final int rows = context.getContentResolver().update( + final int rows = contentResolver.update( WaypointsColumns.CONTENT_URI, createContentValues(waypoint), "_id=" + waypoint.getId(), @@ -546,7 +543,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private Location findLocationBy(String select) { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TrackPointsColumns.CONTENT_URI, null, select, null, null); if (cursor != null && cursor.moveToNext()) { return createLocation(cursor); @@ -570,7 +567,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { private Track findTrackBy(String select) { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, select, null, null); if (cursor != null && cursor.moveToNext()) { return createTrack(cursor); @@ -592,7 +589,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getFirstWaypoint(long trackId) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, "trackid=" + trackId, @@ -614,7 +611,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Waypoint getWaypoint(long waypointId) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, null /*projection*/, "_id=" + waypointId, @@ -637,7 +634,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastLocationId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TrackPointsColumns.CONTENT_URI, projection, "_id=(select max(_id) from trackpoints WHERE trackid=" + trackId + ")", @@ -661,7 +658,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getFirstWaypointId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, projection, "trackid=" + trackId, @@ -685,7 +682,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastWaypointId(long trackId) { final String[] projection = {"_id"}; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( WaypointsColumns.CONTENT_URI, projection, WaypointsColumns.TRACKID + "=" + trackId, @@ -710,7 +707,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { public Track getLastTrack() { Cursor cursor = null; try { - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, "_id=(select max(_id) from tracks)", null, null); if (cursor != null && cursor.moveToNext()) { @@ -729,7 +726,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public long getLastTrackId() { String[] proj = { TracksColumns._ID }; - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TracksColumns.CONTENT_URI, proj, "_id=(select max(_id) from tracks)", null, null); if (cursor != null) { @@ -769,7 +766,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { sortOrder += " LIMIT " + maxLocations; } - return context.getContentResolver().query( + return contentResolver.query( TrackPointsColumns.CONTENT_URI, null, selection, null, sortOrder); } @@ -791,7 +788,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { sortOrder += " LIMIT " + maxWaypoints; } - return context.getContentResolver().query( + return contentResolver.query( WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder); } @@ -847,7 +844,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Cursor getTracksCursor(String selection) { - Cursor cursor = context.getContentResolver().query( + Cursor cursor = contentResolver.query( TracksColumns.CONTENT_URI, null, selection, null, "_id"); return cursor; } @@ -855,14 +852,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public Uri insertTrack(Track track) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrack"); - return context.getContentResolver().insert(TracksColumns.CONTENT_URI, + return contentResolver.insert(TracksColumns.CONTENT_URI, createContentValues(track)); } @Override public Uri insertTrackPoint(Location location, long trackId) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertTrackPoint"); - return context.getContentResolver().insert(TrackPointsColumns.CONTENT_URI, + return contentResolver.insert(TrackPointsColumns.CONTENT_URI, createContentValues(location, trackId)); } @@ -875,14 +872,14 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { values[i] = createContentValues(locations[i], trackId); } - return context.getContentResolver().bulkInsert(TrackPointsColumns.CONTENT_URI, values); + return contentResolver.bulkInsert(TrackPointsColumns.CONTENT_URI, values); } @Override public Uri insertWaypoint(Waypoint waypoint) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.insertWaypoint"); waypoint.setId(-1); - return context.getContentResolver().insert(WaypointsColumns.CONTENT_URI, + return contentResolver.insert(WaypointsColumns.CONTENT_URI, createContentValues(waypoint)); } @@ -891,7 +888,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { Cursor cursor = null; try { final String[] projection = { TracksColumns._ID }; - cursor = context.getContentResolver().query( + cursor = contentResolver.query( TracksColumns.CONTENT_URI, projection, TracksColumns._ID + "=" + id/*selection*/, @@ -911,7 +908,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils { @Override public void updateTrack(Track track) { Log.d(MyTracksProvider.TAG, "MyTracksProviderUtilsImpl.updateTrack"); - context.getContentResolver().update(TracksColumns.CONTENT_URI, + contentResolver.update(TracksColumns.CONTENT_URI, createContentValues(track), "_id=" + track.getId(), null); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java index e89b44e01..571859cc9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/GpxImporter.java @@ -468,8 +468,7 @@ public class GpxImporter extends DefaultHandler { // initialize start time with time of first track point if (statsBuilder == null) { - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(time); + statsBuilder = new TripStatisticsBuilder(time); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java index a74c5778b..802199460 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactory.java @@ -28,7 +28,7 @@ import java.util.Date; /** * Creates a default track name based on the current default track name policy. * - * @author simmonmt@google.com (Matthew Simmons) + * @author Matthew Simmons */ class DefaultTrackNameFactory { private static final String TIMESTAMP_DATE_FORMAT = "yyyy-MM-dd HH:mm"; @@ -60,13 +60,7 @@ class DefaultTrackNameFactory { protected boolean useTimestampTrackName() { SharedPreferences prefs = context.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - - boolean useTimestamp = false; - if (prefs != null) { - useTimestamp = prefs.getBoolean( - context.getString(R.string.timestamp_track_name_key), true); - } - - return useTimestamp; + return prefs.getBoolean( + context.getString(R.string.timestamp_track_name_key), true); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 77e7e7f04..311c17526 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -20,6 +20,7 @@ import com.google.android.apps.mytracks.MyTracksSettings; import com.google.android.maps.mytracks.R; import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; import android.util.Log; /** @@ -30,6 +31,7 @@ import android.util.Log; public class PreferenceManager { private TrackRecordingService service; private final String announcementFrequencyKey; + private final String autoResumeTrackCurrentRetryKey; private final String autoResumeTrackTimeoutKey; private final String maxRecordingDistanceKey; private final String metricUnitsKey; @@ -40,11 +42,22 @@ public class PreferenceManager { private final String signalSamplingFrequencyKey; private final String splitFrequencyKey; + private final SharedPreferences sharedPreferences; + public PreferenceManager(TrackRecordingService service) { this.service = service; - + this.sharedPreferences = + service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); + if (sharedPreferences == null) { + Log.w(MyTracksConstants.TAG, + "TrackRecordingService: Couldn't get shared preferences."); + throw new IllegalStateException("Couldn't get shared preferences"); + } + announcementFrequencyKey = service.getString(R.string.announcement_frequency_key); + autoResumeTrackCurrentRetryKey = + service.getString(R.string.auto_resume_track_current_retry_key); autoResumeTrackTimeoutKey = service.getString(R.string.auto_resume_track_timeout_key); maxRecordingDistanceKey = @@ -57,12 +70,12 @@ public class PreferenceManager { service.getString(R.string.min_recording_interval_key); minRequiredAccuracyKey = service.getString(R.string.min_required_accuracy_key); - splitFrequencyKey = - service.getString(R.string.split_frequency_key); - signalSamplingFrequencyKey = - service.getString(R.string.signal_sampling_frequency_key); recordingTrackKey = service.getString(R.string.recording_track_key); + signalSamplingFrequencyKey = + service.getString(R.string.signal_sampling_frequency_key); + splitFrequencyKey = + service.getString(R.string.split_frequency_key); } /** @@ -72,14 +85,6 @@ public class PreferenceManager { * @param key the key that changed (may be null to update all preferences) */ public void onSharedPreferenceChanged(String key) { - SharedPreferences sharedPreferences = - service.getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - if (sharedPreferences == null) { - Log.w(MyTracksConstants.TAG, - "TrackRecordingService: Couldn't get shared preferences."); - return; - } - if (key == null || key.equals(minRecordingDistanceKey)) { service.setMinRecordingDistance( sharedPreferences.getInt( @@ -156,4 +161,16 @@ public class PreferenceManager { sharedPreferences.getBoolean(metricUnitsKey, true)); } } + + public void setAutoResumeTrackCurrentRetry(int retryAttempts) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts); + editor.commit(); + } + + public void setRecordingTrack(long id) { + Editor editor = sharedPreferences.edit(); + editor.putLong(recordingTrackKey, id); + editor.commit(); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java index 158a978ae..537586f87 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/StatusAnnouncerTask.java @@ -27,6 +27,8 @@ import android.content.Context; import android.content.SharedPreferences; 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; @@ -63,6 +65,26 @@ public class StatusAnnouncerTask implements PeriodicTask { */ private boolean ready = false; + /** + * Whether we're allowed to speak right now. + */ + private boolean speechAllowed; + + /** + * Listener which updates {@link #speechAllowed} when the phone state changes. + */ + private final PhoneStateListener phoneListener = new PhoneStateListener() { + @Override + public void onCallStateChanged(int state, String incomingNumber) { + speechAllowed = state == TelephonyManager.CALL_STATE_IDLE; + + if (!speechAllowed && tts.isSpeaking()) { + // If we're already speaking, stop it. + tts.stop(); + } + } + }; + /** * Constructs the announcer and start the TTS engine. */ @@ -94,8 +116,8 @@ public class StatusAnnouncerTask implements PeriodicTask { int languageAvailability = tts.isLanguageAvailable(speechLanguage); if (languageAvailability == TextToSpeech.LANG_MISSING_DATA || languageAvailability == TextToSpeech.LANG_NOT_SUPPORTED) { - // English is probably supported - // TODO: Somehow use announcement strings from English too + // English is probably supported. + // TODO: Somehow use announcement strings from English too. Log.w(MyTracksConstants.TAG, "Default language not available, using English."); speechLanguage = Locale.ENGLISH; } @@ -116,6 +138,12 @@ public class StatusAnnouncerTask implements PeriodicTask { return; } + if (!speechAllowed) { + Log.i(MyTracksConstants.TAG, + "Not making announcement - not allowed at this time"); + return; + } + if (service == null || service.getTripStatistics() == null) { Log.e(MyTracksConstants.TAG, "StatusAnnouncer stats not initialized."); return; @@ -193,12 +221,23 @@ public class StatusAnnouncerTask implements PeriodicTask { context.getString(speedLabel)); } - public void shutdown() { - tts.shutdown(); + @Override + public void start() { + // Register ourselves as a listener so we won't speak during a call. + speechAllowed = true; + TelephonyManager telephony = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + telephony.listen(phoneListener, PhoneStateListener.LISTEN_CALL_STATE); } @Override - public void start() { + public void shutdown() { + // Stop listening to phone state. + TelephonyManager telephony = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + telephony.listen(phoneListener, PhoneStateListener.LISTEN_NONE); + + tts.shutdown(); } public static int getVolumeStream() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index 2ae835afe..7613e1796 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -28,6 +28,7 @@ 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.ApiFeatures; +import com.google.android.apps.mytracks.util.ApiPlatformAdapter; import com.google.android.apps.mytracks.util.MyTracksUtils; import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.maps.mytracks.R; @@ -69,7 +70,7 @@ 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 static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; + static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3; private NotificationManager notificationManager; private LocationManager locationManager; @@ -96,8 +97,8 @@ public class TrackRecordingService extends Service implements LocationListener { */ private MyTracksProviderUtils providerUtils; - private TripStatisticsBuilder statsBuilder = new TripStatisticsBuilder(); - private TripStatisticsBuilder waypointStatsBuilder = new TripStatisticsBuilder(); + private TripStatisticsBuilder statsBuilder; + private TripStatisticsBuilder waypointStatsBuilder; /** * Current length of the recorded track. This length is calculated from the @@ -331,6 +332,8 @@ public class TrackRecordingService extends Service implements LocationListener { * Shows the notification message and icon in the notification bar. */ public void showNotification() { + final ApiPlatformAdapter apiPlatformAdapter = + ApiFeatures.getInstance().getApiPlatformAdapter(); if (isRecording) { Notification notification = new Notification( R.drawable.arrow_320, null /* tickerText */, @@ -341,9 +344,10 @@ public class TrackRecordingService extends Service implements LocationListener { 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); + apiPlatformAdapter.startForeground(this, notificationManager, 1, + notification); } else { - notificationManager.cancelAll(); + apiPlatformAdapter.stopForeground(this, notificationManager, 1); } } @@ -392,9 +396,11 @@ public class TrackRecordingService extends Service implements LocationListener { } private void restoreStats(Track track) { + Log.d(MyTracksConstants.TAG, + "Restoring stats of track with ID: " + track.getId()); + TripStatistics stats = track.getStatistics(); - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(stats.getStartTime()); + statsBuilder = new TripStatisticsBuilder(stats.getStartTime()); setUpAnnouncer(); signalManager.restore(); @@ -405,11 +411,11 @@ public class TrackRecordingService extends Service implements LocationListener { Waypoint waypoint = providerUtils.getFirstWaypoint(recordingTrackId); if (waypoint != null) { currentWaypointId = waypoint.getId(); - waypointStatsBuilder = new TripStatisticsBuilder(waypoint.getStatistics()); + 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()); + waypointStatsBuilder = new TripStatisticsBuilder(stats.getStartTime()); currentWaypointId = -1; } @@ -433,7 +439,7 @@ public class TrackRecordingService extends Service implements LocationListener { } statsBuilder.getStatistics().setMovingTime(stats.getMovingTime()); statsBuilder.pauseAt(stats.getStopTime()); - statsBuilder.resume(); + statsBuilder.resumeAt(System.currentTimeMillis()); } else { Log.e(MyTracksConstants.TAG, "Could not get track points cursor."); } @@ -658,6 +664,11 @@ public class TrackRecordingService extends Service implements LocationListener { if (recordingTrack != null) { restoreStats(recordingTrack); isRecording = true; + } else { + // Make sure we have consistent state in shared preferences. + Log.w(MyTracksConstants.TAG, "TrackRecordingService.onCreate: Resetting " + + "an orphaned recording track: " + recordingTrackId); + prefManager.setRecordingTrack(recordingTrackId = -1); } showNotification(); } @@ -741,8 +752,10 @@ public class TrackRecordingService extends Service implements LocationListener { // Make sure that the current track exists and is fresh enough. if (recordingTrack == null || !shouldResumeTrack(recordingTrack)) { Log.i(MyTracksConstants.TAG, - "TrackRecordingService: Not resuming because the previous track " - + "doesn't exist or is too old"); + "TrackRecordingService: Not resuming, because the previous track (" + + recordingTrack + ") doesn't exist or is too old"); + isRecording = false; + prefManager.setRecordingTrack(recordingTrackId = -1); stopSelfResult(startId); return; } @@ -755,16 +768,12 @@ public class TrackRecordingService extends Service implements LocationListener { SharedPreferences sharedPreferences, int retryAttempts) { Log.d(MyTracksConstants.TAG, "Updating auto-resume retry attempts to: " + retryAttempts); - - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putInt( - getString(R.string.auto_resume_track_current_retry_key), retryAttempts); - editor.commit(); + prefManager.setAutoResumeTrackCurrentRetry(retryAttempts); } private boolean shouldResumeTrack(Track track) { - Log.d(MyTracksConstants.TAG, - "maybeResumeTrack: autoResumeTrackTimeout = " + autoResumeTrackTimeout); + Log.d(MyTracksConstants.TAG, "shouldResumeTrack: autoResumeTrackTimeout = " + + autoResumeTrackTimeout); // Check if we haven't exceeded the maximum number of retry attempts. SharedPreferences sharedPreferences = @@ -772,11 +781,11 @@ public class TrackRecordingService extends Service implements LocationListener { int retries = sharedPreferences.getInt( getString(R.string.auto_resume_track_current_retry_key), 0); Log.d(MyTracksConstants.TAG, - "TrackRecordingService: Attempting to auto-resume the track (" + "shouldResumeTrack: Attempting to auto-resume the track (" + (retries + 1) + "/" + MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS + ")"); if (retries >= MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS) { Log.i(MyTracksConstants.TAG, - "TrackRecordingService: Not resuming because exceeded the maximum " + "shouldResumeTrack: Not resuming because exceeded the maximum " + "number of auto-resume retries"); return false; } @@ -787,9 +796,13 @@ public class TrackRecordingService extends Service implements LocationListener { // Check for special cases. if (autoResumeTrackTimeout == 0) { // Never resume. + Log.d(MyTracksConstants.TAG, + "shouldResumeTrack: Auto-resume disabled (never resume)"); return false; } else if (autoResumeTrackTimeout == -1) { // Always resume. + Log.d(MyTracksConstants.TAG, + "shouldResumeTrack: Auto-resume forced (always resume)"); return true; } @@ -797,7 +810,8 @@ public class TrackRecordingService extends Service implements LocationListener { long lastModified = track.getStatistics() != null ? track.getStatistics().getStopTime() : 0; Log.d(MyTracksConstants.TAG, - "maybeResumeTrack: lastModified = " + lastModified); + "shouldResumeTrack: lastModified = " + lastModified + + ", autoResumeTrackTimeout: " + autoResumeTrackTimeout); return lastModified > 0 && System.currentTimeMillis() - lastModified <= autoResumeTrackTimeout * 60 * 1000; } @@ -807,6 +821,11 @@ public class TrackRecordingService extends Service implements LocationListener { } public long insertWaypointMarker(Waypoint waypoint) { + if (!isRecording()) { + throw new IllegalStateException( + "Unable to insert waypoint marker while not recording!"); + } + if (waypoint.getLocation() != null) { waypoint.setLength(length); waypoint.setDuration(waypoint.getLocation().getTime() @@ -825,6 +844,11 @@ public class TrackRecordingService extends Service implements LocationListener { * @return the unique id of the inserted marker */ public long insertStatisticsMarker(Location location) { + if (!isRecording()) { + throw new IllegalStateException( + "Unable to insert statistics marker while not recording!"); + } + StringUtils utils = new StringUtils(TrackRecordingService.this); // Create a new waypoint to save @@ -852,8 +876,7 @@ public class TrackRecordingService extends Service implements LocationListener { Uri uri = providerUtils.insertWaypoint(waypoint); // Create a new stats keeper for the next marker - waypointStatsBuilder = new TripStatisticsBuilder(); - waypointStatsBuilder.resumeAt(time); + waypointStatsBuilder = new TripStatisticsBuilder(time); updateCurrentWaypoint(); return Long.parseLong(uri.getLastPathSegment()); } @@ -910,6 +933,10 @@ public class TrackRecordingService extends Service implements LocationListener { @Override public void endCurrentTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.endCurrentTrack"); + if (recordingTrackId == -1 || !isRecording) { + throw new IllegalStateException("No recording track in progress!"); + } + isRecording = false; Track recordingTrack = providerUtils.getTrack(recordingTrackId); if (recordingTrack != null) { @@ -929,12 +956,15 @@ public class TrackRecordingService extends Service implements LocationListener { "_id=" + recordingTrack.getId(), null); } showNotification(); - recordingTrackId = -1; + prefManager.setRecordingTrack(recordingTrackId = -1); } @Override public void deleteAllTracks() { - endCurrentTrack(); + if (isRecording()) { + throw new IllegalStateException( + "Cannot delete all tracks while recording!"); + } providerUtils.deleteAllTracks(); } @@ -953,6 +983,10 @@ public class TrackRecordingService extends Service implements LocationListener { public long startNewTrack() { Log.d(MyTracksConstants.TAG, "TrackRecordingService.startNewTrack"); + if (recordingTrackId != -1 || isRecording) { + throw new IllegalStateException("A track is already in progress!"); + } + long startTime = System.currentTimeMillis(); Track track = new Track(); @@ -964,26 +998,29 @@ public class TrackRecordingService extends Service implements LocationListener { track.setId(recordingTrackId); track.setName(new DefaultTrackNameFactory(this).newTrackName( recordingTrackId, startTime)); - providerUtils.updateTrack(track); - - currentWaypointId = insertStatisticsMarker(null); isRecording = true; isMoving = true; - statsBuilder = new TripStatisticsBuilder(); - statsBuilder.resumeAt(startTime); + + providerUtils.updateTrack(track); + statsBuilder = new TripStatisticsBuilder(startTime); + waypointStatsBuilder = new TripStatisticsBuilder(startTime); + currentWaypointId = insertStatisticsMarker(null); setUpAnnouncer(); length = 0; showNotification(); registerLocationListener(); splitManager.restore(); signalManager.restore(); + // Reset the number of auto-resume retries. - SharedPreferences sharedPreferences = - getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0); - setAutoResumeTrackRetries(sharedPreferences, 0); + setAutoResumeTrackRetries( + getSharedPreferences(MyTracksSettings.SETTINGS_NAME, 0), 0); + // Persist the current recording track. + prefManager.setRecordingTrack(recordingTrackId); + return recordingTrackId; } - + TripStatistics getTripStatistics() { return statsBuilder.getStatistics(); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java index f0107e832..8882ae7d2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java +++ b/MyTracks/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilder.java @@ -36,13 +36,13 @@ public class TripStatisticsBuilder { /** * The last location that the gps reported. */ - private Location lastLocation = null; + private Location lastLocation; /** * The last location that contributed to the stats. It is also the last * location the user was found to be moving. */ - private Location lastMovingLocation = null; + private Location lastMovingLocation; /** * The current speed in meters/second as reported by the gps. @@ -90,10 +90,13 @@ public class TripStatisticsBuilder { private long totalLocations = 0; /** - * Creates a new trip starting at the current system time. + * Creates a new trip starting at the given time. + * + * @param startTime the start time. */ - public TripStatisticsBuilder() { + public TripStatisticsBuilder(long startTime) { data = new TripStatistics(); + resumeAt(startTime); } /** @@ -103,6 +106,9 @@ public class TripStatisticsBuilder { */ public TripStatisticsBuilder(TripStatistics statsData) { data = new TripStatistics(statsData); + if (data.getStartTime() > 0) { + resumeAt(data.getStartTime()); + } } /** @@ -140,8 +146,8 @@ public class TripStatisticsBuilder { // Don't do anything if we didn't move since last fix: double distance = lastLocation.distanceTo(currentLocation); - if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE - && currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) { + if (distance < MyTracksConstants.MAX_NO_MOVEMENT_DISTANCE && + currentSpeed < MyTracksConstants.MAX_NO_MOVEMENT_SPEED) { lastLocation = currentLocation; return false; } @@ -291,13 +297,6 @@ public class TripStatisticsBuilder { data.updateGradeExtremities(gradeBuffer.getAverage()); } - /** - * Pauses the track at the current time. - */ - public void pause() { - pauseAt(System.currentTimeMillis()); - } - /** * Pauses the track at the given time. * @@ -312,13 +311,6 @@ public class TripStatisticsBuilder { paused = true; } - /** - * Resumes the current track at the current time. - */ - public void resume() { - resumeAt(System.currentTimeMillis()); - } - /** * Resumes the current track at the given time. * @@ -364,8 +356,7 @@ public class TripStatisticsBuilder { } public TripStatistics getStatistics() { - // Take a snapshot - we do't want anyone messing with our internals + // Take a snapshot - we don't want anyone messing with our internals return new TripStatistics(data); } } - diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java index b6a4e02fd..7979e12b5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java @@ -15,7 +15,10 @@ */ package com.google.android.apps.mytracks.util; +import com.google.android.apps.mytracks.MyTracksConstants; + import android.os.Build; +import android.util.Log; /** * Utility class for determining if newer-API features are available on the @@ -28,9 +31,15 @@ public class ApiFeatures { /** * The API level of the Android version we're being run under. */ - public static final int ANDROID_API_LEVEL = Integer.parseInt(Build.VERSION.SDK); - + public static final int ANDROID_API_LEVEL = Integer.parseInt( + Build.VERSION.SDK); + private static ApiFeatures instance; + + /** + * The API platform adapter supported by this system. + */ + private ApiPlatformAdapter apiPlatformAdapter; /** * Returns the singleton instance of this class. @@ -52,7 +61,28 @@ public class ApiFeatures { /** * Allow subclasses for mocking, but no direct instantiation. */ - protected ApiFeatures() {} + protected ApiFeatures() { + if (getApiLevel() >= 5) { + try { + Class clazz = Class.forName( + "com.google.android.apps.mytracks.util.EclairPlatformAdapter"); + apiPlatformAdapter = (ApiPlatformAdapter) clazz.newInstance(); + } catch (Exception e) { + Log.i(MyTracksConstants.TAG, "ApiFeatures: Unable to instantiate Eclair" + + " platform adapter", e); + } + } + if (apiPlatformAdapter == null) { + Log.i(MyTracksConstants.TAG, + "ApiFeatures: Using default platform adapter"); + // Cupcake adapter is always supported, so it's safe to do static linkage. + apiPlatformAdapter = new CupcakePlatformAdapter(); + } + } + + public ApiPlatformAdapter getApiPlatformAdapter() { + return apiPlatformAdapter; + } /** * Returns whether cloud backup (a.k.a. Froyo backup) is available. @@ -81,7 +111,7 @@ public class ApiFeatures { public boolean hasModernSignalStrength() { return getApiLevel() >= 7; } - + // Visible for testing. protected int getApiLevel() { return ANDROID_API_LEVEL; diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java new file mode 100644 index 000000000..1e2a5210a --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiPlatformAdapter.java @@ -0,0 +1,60 @@ +/* + * 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.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * A set of methods that may be implemented in a platform specific way. + * + * @author Bartlomiej Niechwiej + */ +public interface ApiPlatformAdapter { + + /** + * Puts the specified service into foreground. + * + * @param service the service to be put in foreground. + * @param notificationManager the notification manager used to post the given + * notification. + * @param id the ID of the notification, unique within the application. + * @param notification the notification to post. + */ + void startForeground(Service service, NotificationManager notificationManager, + int id, Notification notification); + + /** + * Puts the given service into background. + * @param service the service to put into background. + * @param notificationManager the notification manager to user when removing + * notifications. + * @param id the ID of the notification to be remove, or -1 if the + * notification shouldn't be removed. + */ + void stopForeground(Service service, NotificationManager notificationManager, + int id); + + /** + * Terminates the given handler thread. + * + * @param handlerThread the thread to be terminated. + * @return true whether the handler has been stopped or not. + */ + boolean stopHandlerThread(HandlerThread handlerThread); +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java new file mode 100644 index 000000000..272b47218 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/CupcakePlatformAdapter.java @@ -0,0 +1,53 @@ +/* + * 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.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * The Cupcake (API level 3) specific implementation of the + * {@link ApiPlatformAdapter}. + * + * @author Bartlomiej Niechwiej + */ +public class CupcakePlatformAdapter implements ApiPlatformAdapter { + + @Override + public void startForeground(Service service, + NotificationManager notificationManager, int id, + Notification notification) { + service.setForeground(true); + notificationManager.notify(id, notification); + } + + @Override + public void stopForeground(Service service, + NotificationManager notificationManager, int id) { + service.setForeground(false); + if (id != -1) { + notificationManager.cancel(id); + } + } + + @Override + public boolean stopHandlerThread(HandlerThread handlerThread) { + // Do nothing, as Cupcake doesn't provide quit(). + return false; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java new file mode 100644 index 000000000..9c32946d3 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EclairPlatformAdapter.java @@ -0,0 +1,48 @@ +/* + * 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.util; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.os.HandlerThread; + +/** + * The Eclair (API level 5) specific implementation of the + * {@link ApiPlatformAdapter}. + * + * @author Bartlomiej Niechwiej + */ +public class EclairPlatformAdapter implements ApiPlatformAdapter { + + @Override + public void startForeground(Service service, + NotificationManager notificationManager, int id, + Notification notification) { + service.startForeground(id, notification); + } + + @Override + public void stopForeground(Service service, + NotificationManager notificationManager, int id) { + service.stopForeground(id != -1); + } + + @Override + public boolean stopHandlerThread(HandlerThread handlerThread) { + return handlerThread.quit(); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java index c2a380e88..6c6ca7af0 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/FileUtils.java @@ -37,7 +37,7 @@ public class FileUtils { * A set of characters that are prohibited from being in file names. */ private static final Pattern PROHIBITED_CHAR_PATTERN = - Pattern.compile("[^ A-Za-z0-9_.()]+"); + Pattern.compile("[^ A-Za-z0-9_.()-]+"); /** * Builds a path inside the My Tracks directory in the SD card. @@ -69,8 +69,6 @@ public class FileUtils { * Normalizes the input string and make sure it is a valid fat32 file name. * * @param name the name to normalize - * @param overheadSize the number of additional characters that will be added - * to the name after sanitization * @return the sanitized name */ String sanitizeName(String name) { diff --git a/MyTracksTest/.factorypath b/MyTracksTest/.factorypath index 25bd193ed..fefa9e647 100644 --- a/MyTracksTest/.factorypath +++ b/MyTracksTest/.factorypath @@ -1,4 +1,7 @@ + + + diff --git a/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs b/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs index 357ffd6b8..ac3541cd4 100644 --- a/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs +++ b/MyTracksTest/.settings/org.eclipse.jdt.apt.core.prefs @@ -1,7 +1,7 @@ -#Thu Aug 12 19:30:48 BRT 2010 +#Mon Nov 01 02:42:03 BRST 2010 eclipse.preferences.version=1 org.eclipse.jdt.apt.aptEnabled=true org.eclipse.jdt.apt.genSrcDir=.apt_generated -org.eclipse.jdt.apt.processorOptions/bin_dir=MyTracksTest/bin -org.eclipse.jdt.apt.processorOptions/logfile=MyTracksTest/annotations.log +org.eclipse.jdt.apt.processorOptions/bin_dir=%PROJECT.DIR%/bin +org.eclipse.jdt.apt.processorOptions/logfile=%PROJECT.DIR%/annotations.log org.eclipse.jdt.apt.reconcileEnabled=false diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java new file mode 100644 index 000000000..afefd4910 --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/ChartValueSeriesTest.java @@ -0,0 +1,94 @@ +/* + * 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; + +import com.google.android.maps.mytracks.R; + +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.test.AndroidTestCase; + +/** + * @author Sandor Dornbush + */ +public class ChartValueSeriesTest extends AndroidTestCase { + private ChartValueSeries series; + private Paint fillPaint1; + + @Override + protected void setUp() throws Exception { + fillPaint1 = new Paint(); + fillPaint1.setStyle(Style.FILL); + fillPaint1.setColor(getContext().getResources().getColor(R.color.green)); + fillPaint1.setAntiAlias(true); + series = new ChartValueSeries(getContext(), + "###,###", + fillPaint1, + null, + 100, + R.string.elevation); + } + + public void testInitialConditions() { + assertEquals(25, series.getInterval()); + assertEquals(1, series.getMaxLabelLength()); + assertEquals(0, series.getMin()); + assertEquals(0, series.getMax()); + assertEquals(0.0, series.getSpread()); + assertEquals(fillPaint1, series.getPaint()); + assertEquals(getContext().getString(R.string.elevation), + series.getTitle()); + assertTrue(series.isEnabled()); + } + + public void testEnabled() { + series.setEnabled(false); + assertFalse(series.isEnabled()); + } + + public void testSmallUpdates() { + series.update(0); + series.update(10); + series.updateDimension(); + assertEquals(25, series.getInterval()); + assertEquals(3, series.getMaxLabelLength()); + assertEquals(0, series.getMin()); + assertEquals(100, series.getMax()); + assertEquals(100.0, series.getSpread()); + } + + public void testBigUpdates() { + series.update(0); + series.update(901); + series.updateDimension(); + assertEquals(200, series.getInterval()); + assertEquals(5, series.getMaxLabelLength()); + assertEquals(0, series.getMin()); + assertEquals(1000, series.getMax()); + assertEquals(1000.0, series.getSpread()); + } + + public void testNotZeroBasedUpdates() { + series.update(500); + series.update(1401); + series.updateDimension(); + assertEquals(200, series.getInterval()); + assertEquals(5, series.getMaxLabelLength()); + assertEquals(500, series.getMin()); + assertEquals(1500, series.getMax()); + assertEquals(1000.0, series.getSpread()); + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java index 62e529a14..855619cb7 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/DefaultTrackNameFactoryTest.java @@ -21,7 +21,16 @@ import android.test.AndroidTestCase; import java.text.SimpleDateFormat; import java.util.Date; +/** + * Tests {@link DefaultTrackNameFactory} + * + * @author Matthew Simmons + */ public class DefaultTrackNameFactoryTest extends AndroidTestCase { + /** + * A version of the factory which allows us to supply our own answer as to + * whether a timestamp-based track name should be used. + */ private static class MockDefaultTrackNameFactory extends DefaultTrackNameFactory { private final boolean useTimestamp; diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 8a768f637..fc7ae5396 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -15,12 +15,33 @@ */ package com.google.android.apps.mytracks.services; +import static com.google.android.apps.mytracks.MyTracksConstants.RESUME_TRACK_EXTRA_NAME; + +import com.google.android.apps.mytracks.MyTracksSettings; +import com.google.android.apps.mytracks.content.MyTracksProvider; +import com.google.android.apps.mytracks.content.MyTracksProviderUtils; +import com.google.android.apps.mytracks.content.Track; +import com.google.android.apps.mytracks.content.Waypoint; +import com.google.android.apps.mytracks.stats.TripStatistics; +import com.google.android.apps.mytracks.util.ApiFeatures; +import com.google.android.maps.mytracks.R; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.location.Location; import android.os.IBinder; +import android.test.RenamingDelegatingContext; import android.test.ServiceTestCase; +import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.MediumTest; import android.test.suitebuilder.annotation.SmallTest; +import java.util.List; + /** * Tests for the MyTracks track recording service. * @@ -28,24 +49,513 @@ import android.test.suitebuilder.annotation.SmallTest; */ public class TrackRecordingServiceTest extends ServiceTestCase { - + + private Context context; + private MyTracksProviderUtils providerUtils; + private SharedPreferences sharedPreferences; + public TrackRecordingServiceTest() { super(TrackRecordingService.class); } + + /** + * A context wrapper with the user provided {@link ContentResolver}. + */ + private static class MockContext extends ContextWrapper { + private final ContentResolver contentResolver; + + public MockContext(ContentResolver contentResolver, Context base) { + super(base); + this.contentResolver = contentResolver; + } + + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } + } + + /** + * A mock class that forces API level < 5 to make sure we can workaround a bug + * in ServiceTestCase (throwing a NPE). + * See http://code.google.com/p/android/issues/detail?id=12122 for more + * details. + */ + private static class MockApiFeatures extends ApiFeatures { + @Override + protected int getApiLevel() { + return 4; + } + } + @Override + protected void setUp() throws Exception { + super.setUp(); + + ApiFeatures.injectInstance(new MockApiFeatures()); + + MockContentResolver mockContentResolver = new MockContentResolver(); + RenamingDelegatingContext targetContext = new RenamingDelegatingContext( + getContext(), getContext(), "test."); + context = new MockContext(mockContentResolver, targetContext); + MyTracksProvider provider = new MyTracksProvider(); + provider.attachInfo(context, null); + mockContentResolver.addProvider(MyTracksProviderUtils.AUTHORITY, provider); + setContext(context); + + providerUtils = MyTracksProviderUtils.Factory.get(context); + + sharedPreferences = context.getSharedPreferences( + MyTracksSettings.SETTINGS_NAME, 0); + // Disable auto resume by default. + updateAutoResumePrefs(0, -1); + // No recording track. + Editor editor = sharedPreferences.edit(); + editor.putLong(context.getString(R.string.recording_track_key), -1); + editor.commit(); + } + @SmallTest public void testStartable() { - Intent startIntent = new Intent(); - startIntent.setClass(getContext(), TrackRecordingService.class); - startService(startIntent); + startService(createStartIntent()); assertNotNull(getService()); } @MediumTest public void testBindable() { - Intent startIntent = new Intent(); - startIntent.setClass(getContext(), TrackRecordingService.class); - IBinder service = bindService(startIntent); + IBinder service = bindService(createStartIntent()); assertNotNull(service); } + + @MediumTest + public void testResumeAfterReboot_shouldResume() throws Exception { + // Insert a dummy track and mark it as recording track. + createDummyTrack(123, System.currentTimeMillis(), true); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We expect to resume the previous track. + assertTrue(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(123, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_simulateReboot() throws Exception { + updateAutoResumePrefs(0, 10); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Simulate recording a track. + long id = service.startNewTrack(); + assertTrue(service.isRecording()); + assertEquals(id, service.getRecordingTrackId()); + shutdownService(); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // TODO: shutdownService() has a bug and doesn't set mServiceCreated + // to false, thus preventing from a second call to onCreate(). + // Report the bug to Android team. Until then, the following check + // must be commented out. + + // assertTrue(getService().isRecording()); + } + + @MediumTest + public void testResumeAfterReboot_noRecordingTrack() throws Exception { + // Insert a dummy track and mark it as recording track. + createDummyTrack(123, System.currentTimeMillis(), false); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because it was stopped. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_expiredTrack() throws Exception { + // Insert a dummy track last updated 20 min ago. + createDummyTrack(123, System.currentTimeMillis() - 20 * 60 * 1000, true); + + // Clear the number of attempts and set the timeout to 10 min. + updateAutoResumePrefs(0, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because it has expired. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testResumeAfterReboot_tooManyAttempts() throws Exception { + // Insert a dummy track. + createDummyTrack(123, System.currentTimeMillis(), true); + + // Set the number of attempts to max. + updateAutoResumePrefs( + TrackRecordingService.MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS, 10); + + // Start the service in "resume" mode (simulates the on-reboot action). + Intent startIntent = createStartIntent(); + startIntent.putExtra(RESUME_TRACK_EXTRA_NAME, true); + startService(startIntent); + assertNotNull(getService()); + + // We don't expect to resume the previous track, because there were already + // too many attempts. + assertFalse(getService().isRecording()); + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_noTracks() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + // Test if we start in no-recording mode by default. + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_oldTracks() throws Exception { + createDummyTrack(123, -1, false); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testRecording_orphanedRecordingTrack() throws Exception { + // Just set recording track to a bogus value. + setRecordingTrack(256); + + // Make sure that the service will not start recording and will clear + // the bogus track. + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testStartNewTrack_noRecording() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + long id = service.startNewTrack(); + assertTrue(id >= 0); + assertTrue(service.isRecording()); + Track track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + assertEquals(id, service.getRecordingTrackId()); + } + + @MediumTest + public void testStartNewTrack_alreadyRecording() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + try { + service.startNewTrack(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertEquals(123, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(123, service.getRecordingTrackId()); + } + + @MediumTest + public void testEndCurrentTrack_alreadyRecording() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + // End the current track. + service.endCurrentTrack(); + assertFalse(service.isRecording()); + assertEquals(-1, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testEndCurrentTrack_noRecording() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // End the current track. + try { + service.endCurrentTrack(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertEquals(-1, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), 0)); + assertEquals(-1, service.getRecordingTrackId()); + } + + @MediumTest + public void testIntegration_completeRecordingSession() throws Exception { + List tracks = providerUtils.getAllTracks(); + assertTrue(tracks.isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Start a track. + long id = service.startNewTrack(); + assertTrue(id >= 0); + assertTrue(service.isRecording()); + Track track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + assertEquals(id, sharedPreferences.getLong( + context.getString(R.string.recording_track_key), -1)); + assertEquals(id, service.getRecordingTrackId()); + + // Stop the track. Validate if it has correct data. + service.endCurrentTrack(); + assertFalse(service.isRecording()); + assertEquals(-1, service.getRecordingTrackId()); + track = providerUtils.getTrack(id); + assertNotNull(track); + assertEquals(id, track.getId()); + TripStatistics tripStatistics = track.getStatistics(); + assertNotNull(tripStatistics); + assertTrue(tripStatistics.getStartTime() > 0); + assertTrue(tripStatistics.getStopTime() >= tripStatistics.getStartTime()); + } + + @MediumTest + public void testDeleteAllTracks_noRecording() throws Exception { + createDummyTrack(123, -1, false); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Deleting all tracks should succeed. + service.deleteAllTracks(); + assertFalse(service.isRecording()); + assertTrue(providerUtils.getAllTracks().isEmpty()); + } + + @MediumTest + public void testDeleteAllTracks_noTracks() throws Exception { + assertTrue(providerUtils.getAllTracks().isEmpty()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + // Deleting all tracks should succeed. + service.deleteAllTracks(); + assertFalse(service.isRecording()); + assertTrue(providerUtils.getAllTracks().isEmpty()); + } + + @MediumTest + public void testDeleteAllTracks_trackInProgress() throws Exception { + createDummyTrack(123, -1, true); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + // Since we have a track in progress, we expect to fail. + try { + service.deleteAllTracks(); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + assertTrue(service.isRecording()); + assertEquals(1, providerUtils.getAllTracks().size()); + } + + @MediumTest + public void testHasRecorded_noTracks() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertFalse(service.hasRecorded()); + } + + @MediumTest + public void testHasRecorded_trackInProgress() throws Exception { + createDummyTrack(123, -1, true); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + assertTrue(service.hasRecorded()); + } + + @MediumTest + public void testHasRecorded_oldTracks() throws Exception { + createDummyTrack(123, -1, false); + assertEquals(1, providerUtils.getAllTracks().size()); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + assertTrue(service.hasRecorded()); + } + + @MediumTest + public void testInsertStatisticsMarker_noRecordingTrack() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + Location loc = new Location("gps"); + try { + service.insertStatisticsMarker(loc); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + } + + @MediumTest + public void testInsertStatisticsMarker_validLocation() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Location loc = new Location("gps"); + assertEquals(1, service.insertStatisticsMarker(loc)); + assertEquals(2, service.insertStatisticsMarker(loc)); + + // TODO: Add more checks. + } + + @MediumTest + public void testInsertWaypointMarker_noRecordingTrack() throws Exception { + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertFalse(service.isRecording()); + + Location loc = new Location("gps"); + Waypoint waypoint = new Waypoint(); + waypoint.setId(1); + waypoint.setLocation(loc); + try { + service.insertWaypointMarker(waypoint); + fail("Expecting IllegalStateException"); + } catch (IllegalStateException e) { + // Expected. + } + } + + @MediumTest + public void testInsertWaypointMarker_invalidWaypoint() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Waypoint waypoint = new Waypoint(); + assertEquals(-1, service.insertWaypointMarker(waypoint)); + } + + @MediumTest + public void testInsertWaypointMarker_validWaypoint() throws Exception { + createDummyTrack(123, -1, true); + + ITrackRecordingService service = bindAndGetService(createStartIntent()); + assertTrue(service.isRecording()); + + Location loc = new Location("gps"); + Waypoint waypoint = new Waypoint(); + waypoint.setId(1); + waypoint.setLocation(loc); + assertEquals(1, service.insertWaypointMarker(waypoint)); + } + + private ITrackRecordingService bindAndGetService(Intent intent) { + ITrackRecordingService service = ITrackRecordingService.Stub.asInterface( + bindService(intent)); + assertNotNull(service); + return service; + } + + private Track createDummyTrack(long id, long stopTime, boolean isRecording) { + Track dummyTrack = new Track(); + dummyTrack.setId(id); + dummyTrack.setName("Dummy Track"); + TripStatistics tripStatistics = new TripStatistics(); + tripStatistics.setStopTime(stopTime); + dummyTrack.setStatistics(tripStatistics); + addTrack(dummyTrack, isRecording); + return dummyTrack; + } + + private void updateAutoResumePrefs(int attempts, int timeoutMins) { + Editor editor = sharedPreferences.edit(); + editor.putInt(context.getString( + R.string.auto_resume_track_current_retry_key), attempts); + editor.putInt(context.getString( + R.string.auto_resume_track_timeout_key), timeoutMins); + editor.commit(); + } + + private Intent createStartIntent() { + Intent startIntent = new Intent(); + startIntent.setClass(context, TrackRecordingService.class); + return startIntent; + } + + private void addTrack(Track track, boolean isRecording) { + assertTrue(track.getId() >= 0); + providerUtils.insertTrack(track); + assertEquals(track.getId(), providerUtils.getTrack(track.getId()).getId()); + setRecordingTrack(isRecording ? track.getId() : -1); + } + + private void setRecordingTrack(long id) { + Editor editor = sharedPreferences.edit(); + editor.putLong(context.getString(R.string.recording_track_key), id); + editor.commit(); + } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java index f19b2da90..ccb3fa858 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/stats/TripStatisticsBuilderTest.java @@ -20,13 +20,11 @@ public class TripStatisticsBuilderTest extends TestCase { @Override protected void setUp() throws Exception { super.setUp(); - builder = new TripStatisticsBuilder(); - builder.resume(); + builder = new TripStatisticsBuilder(System.currentTimeMillis()); } public void testAddLocationSimple() throws Exception { - builder = new TripStatisticsBuilder(); - builder.resumeAt(1000); + builder = new TripStatisticsBuilder(1000); TripStatistics stats = builder.getStatistics(); assertEquals(0.0, builder.getSmoothedElevation()); @@ -84,8 +82,7 @@ public class TripStatisticsBuilderTest extends TestCase { */ public void testElevationSimple() throws Exception { for (double elevation = 0; elevation < 1000; elevation += 10) { - builder = new TripStatisticsBuilder(); - builder.resume(); + builder = new TripStatisticsBuilder(System.currentTimeMillis()); for (int j = 0; j < 100; j++) { assertEquals(0.0, builder.updateElevation(elevation)); assertEquals(elevation, builder.getSmoothedElevation()); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java index 1f120cbf2..f47ab6dbb 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java @@ -75,4 +75,8 @@ public class ApiFeaturesTest extends TestCase { features.setApiLevel(8); assertTrue(features.hasModernSignalStrength()); } + + public void testGetApiPlatformAdapter() { + assertNotNull(features.getApiPlatformAdapter()); + } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java index 715370bab..01b44c6b6 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/FileUtilsTest.java @@ -31,8 +31,8 @@ import junit.framework.TestCase; * @author Rodrigo Damazio */ public class FileUtilsTest extends TestCase { - private static final String ORIGINAL_NAME = "Swim\10ming ^across: the/ pacific (ocean)."; - private static final String SANITIZED_NAME = "Swimming across the pacific (ocean)."; + private static final String ORIGINAL_NAME = "Swim\10ming-^across: the/ pacific (ocean)."; + private static final String SANITIZED_NAME = "Swimming-across the pacific (ocean)."; private FileUtils fileUtils; private Set existingFiles;