Ignoring Matthew's second head by no-op merging it.

This commit is contained in:
Rodrigo Damazio
2010-11-03 20:53:50 -02:00
33 changed files with 1267 additions and 313 deletions
+1
View File
@@ -3,3 +3,4 @@ syntax:glob
*/bin
*/gen/**
.metadata
MyTracksTest/annotations.log
+10 -10
View File
@@ -1,10 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/googleloginclient-helper.jar"/>
<classpathentry kind="lib" path="libs/google-common.jar"/>
<classpathentry kind="lib" path="libs/accounts.jar"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin"/>
</classpath>
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry kind="lib" path="libs/googleloginclient-helper.jar"/>
<classpathentry kind="lib" path="libs/google-common.jar"/>
<classpathentry kind="lib" path="libs/accounts.jar"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="output" path="bin"/>
</classpath>
+5 -1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.android.maps.mytracks"
android:installLocation="auto"
android:versionCode="20"
android:versionName="1.0.19">
<application android:icon="@drawable/arrow_icon"
@@ -12,7 +13,7 @@
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:multiprocess="true"
android:process=":trackrecord"
android:authorities="com.google.android.maps.mytracks" />
<activity android:name="com.google.android.apps.mytracks.MyTracks"
@@ -101,6 +102,9 @@
<uses-permission android:name="com.google.android.googleapps.permission.GOOGLE_AUTH.writely" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" />
<!-- Permissions for misc services -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-sdk android:minSdkVersion="3"/>
<supports-screens
android:largeScreens="true"
+8
View File
@@ -147,6 +147,14 @@ limitations under the License.
<item>1 分钟</item>
</string-array>
<!-- Auto-resume track timeout -->
<string-array name="auto_resume_track_timeout_options">
<item>从不</item>
<item>5 分钟</item>
<item>10 分钟</item>
<item>15 分钟</item>
<item>总是</item>
</string-array>
<!-- Other non-preference arrays -->
<array name="activity_types">
<item>未知</item>
+106 -26
View File
@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- DO NOT TRANSLATE -->
<string name="app_name">My Tracks</string>
<!-- Used by the Menu -->
<string name="start_recording">记录轨迹</string>
@@ -32,6 +32,7 @@ limitations under the License.
<string name="choose_track_message">请选择要查看的轨迹</string>
<string name="current_track">当前轨迹</string>
<string name="current_segment">上一段轨迹</string>
<string name="aggregated_stats">累积统计</string>
<!-- Error messages -->
<string name="error_no_location_manager">此设备上没有位置管理器!</string>
@@ -122,6 +123,7 @@ limitations under the License.
<string name="tracklist_save_as_gpx">保存为 GPX 文件</string>
<string name="tracklist_save_as_kml">保存为 KML 文件</string>
<string name="tracklist_save_as_csv">保存为 CSV 文件</string>
<string name="tracklist_save_as_tcx">保存为 TCX 文件</string>
<string name="tracklist_btn_send_all_to_mymaps">发送到 Google My Maps</string>
<string name="tracklist_btn_delete_all">删除所有轨迹</string>
<!-- Export All -->
@@ -135,6 +137,7 @@ limitations under the License.
<string name="tracklist_share_gpx_file">GPX 文件</string>
<string name="tracklist_share_kml_file">KML 文件</string>
<string name="tracklist_share_csv_file">CSV 文件</string>
<string name="tracklist_share_tcx_file">TCX 文件</string>
<string name="tracklist_clear_map">清除地图</string>
<!-- Waypoints list -->
@@ -165,9 +168,10 @@ limitations under the License.
<string name="settings_recording">记录</string>
<string name="settings_sharing">分享</string>
<string name="settings_not_available_summary">你使用的 Android 版本无法支持</string>
<string name="settings_announcement_frequency">提示频度</string>
<string name="settings_announcement_frequency_summary">语音提示的最小时间间隔</string>
<string name="settings_min_recording_distance">最小记录间距</string>
<string name="settings_min_recording_distance_summary">位置记录的最小间隔距离</string>
@@ -202,7 +206,38 @@ limitations under the License.
<string name="settings_split_frequency">切割频度</string>
<string name="settings_split_frequency_summary">插入新切割标记所需要达到的最大时间或距离</string>
<string name="pace_label">步数</string>
<string name="settings_auto_resume_track_timeout">自动恢复追踪等待时间</string>
<string name="settings_auto_resume_track_timeout_summary">手机重启后自动继续上次轨迹记录的最大间隔时间</string>
<string name="pace_label">节奏</string>
<string name="eclair_required">只在 Android 2.0 及以上版本支持</string>
<string name="settings_sensor_type">感应器类型</string>
<string name="settings_sensor_type_summary">要连接到的感应器类型</string>
<string name="settings_sensor_id">感应器ID</string>
<string name="settings_sensor_id_summary">发送到感应器的ID.</string>
<string name="sensor_id_malformed">ID必须是一个整数值.</string>
<string name="bluetooth_sensor_title">蓝牙感应器</string>
<string name="bluetooth_sensor_summary">要连接到的蓝牙感应器</string>
<string name="settings_backup">备份 (测试版)</string>
<string name="settings_sd_backup">SD 存储卡备份</string>
<string name="settings_backup_to_sd">立即备份</string>
<string name="settings_backup_to_sd_summary">将你所有轨迹和设置的完整备份写入到 SD 存储卡</string>
<string name="settings_restore_from_sd">立即恢复</string>
<string name="settings_restore_from_sd_summary">从 SD 存储卡恢复所有轨迹和设置</string>
<string name="settings_no_backup_while_recording">在记录轨迹时无法处理备份数据.</string>
<!-- Backups -->
<string name="restore_overwrites_warning">从 SD 存储卡恢复备份数据将覆盖掉你当前的所有轨迹和设置. 确定要继续吗?</string>
<string name="select_backup_to_restore">请选择要恢复的备份文件</string>
<string name="no_backups">没有找到备份文件.</string>
<string name="backup_write_progress_message">将所有轨迹和设置写入 SD 存储卡中. 这需要一些时间...</string>
<string name="backup_import_progress_message">从 SD 存储卡读取轨迹和设置中. 这需要一些时间...</string>
<!-- MyTracksSky -->
<string name="mytracks_sky">GPS 卫星</string>
@@ -241,7 +276,7 @@ limitations under the License.
<!-- Visibility -->
<string name="public_map">公开</string>
<string name="unlisted_map">Unlisted</string>
<string name="unlisted_map">未公布</string>
<string name="mapslist_no_maps_available">你还没有保存任何地图.</string>
<!-- Statistics View -->
@@ -292,24 +327,32 @@ limitations under the License.
<string name="second">秒</string>
<string name="seconds">秒</string>
<!-- Distance -->
<string name="meter">m</string>
<string name="kilometer">km</string>
<string name="kilometers_long">千米</string>
<string name="feet">ft</string>
<string name="mile">mi</string>
<string name="miles_long">英里</string>
<!-- Speed and Pace -->
<string name="kilometer_per_hour">km/h</string>
<string name="kilometer_per_hour_long">千米每小时</string>
<string name="min_per_kilometer">min/km</string>
<string name="per_kilometer">每千米</string>
<string name="mile_per_hour">mi/h</string>
<string name="mile_per_hour_long">英里每小时</string>
<string name="min_per_mile">min/mi</string>
<string name="per_mile">每英里</string>
<!-- Stats Labels -->
<string name="moving_time_label">移动时间</string>
<string name="total_time_label">总时间</string>
@@ -320,7 +363,11 @@ limitations under the License.
<string name="statistics">统计</string>
<string name="total_distance_label">总距离</string>
<string name="distance_label">距离</string>
<string name="unknown">-</string>
<string name="power">电源</string>
<string name="cadence">节奏</string>
<string name="heart_rate">心率</string>
<string name="max_heart_rate">最大心率</string>
<string name="waiting">请稍候...</string>
<!-- End User License Agreement -->
@@ -336,20 +383,27 @@ limitations under the License.
<string name="about_email">技术支持</string>
<string name="about_support">技术支持:</string>
<!-- DO NOT TRANSLATE -->
<string name="version">1.0.14</string>
<string name="about_googlegroups">mytracks@googlegroups.com</string>
<string name="about_weburl">mytracks.appspot.com</string>
<string name="about_copyright_message">Copyright 2009 Google Inc.</string>
<string name="about_dylan">Dylan Casey</string>
<string name="about_leif">Leif Hendrik Wilden</string>
<string name="about_rdamazio">Rodrigo Damazio Bovendorp</string>
<string name="about_sandor">Sandor Dornbush</string>
<!-- Send to Google -->
<string name="sendtogoogle_title">发送到...</string>
<string name="sendtogoogle_google_mymaps">Google My Maps</string>
<string name="sendtogoogle_google_docs">Google Docs</string>
<string name="sendtogoogle_send_now">立即发送</string>
<string name="sendtogoogle_create_new_map">创建新地图</string>
<string name="sendtogoogle_pick_existing_map">选择已有地图</string>
@@ -364,8 +418,9 @@ limitations under the License.
<!-- SD card -->
<string name="io_no_external_storage_found">没有发现存贮设备. 请确认插入了 SD 卡并且你的手机没有将挂载 SD 卡到电脑.</string>
<string name="io_write_failed">无法写入文件.</string>
<string name="io_create_dir_failed">无法在 SD 卡上创建目录.</string>
<string name="io_create_dir_failed">无法在 SD 存储卡上创建目录.</string>
<string name="io_write_finished">轨迹已导出到 SD 卡.</string>
<string name="io_read_finished">从 SD 存储卡读取完毕.</string>
<!-- VersionChecker -->
<string name="version_check_title">检查更新</string>
@@ -403,4 +458,29 @@ limitations under the License.
<string name="gsm_strength">GSM 强度 %1$d</string>
<string name="cdma_strength">CDMA 强度 %1$d dBm</string>
<string name="evdo_strength">EVDO 强度 %1$d dBm</string>
<!-- Fake preference keys - never saved, but show options in the preferences screen -->
<string name="method_bluetooth">蓝牙</string>
<string name="bluetooth_options">蓝牙设置</string>
<string name="bluetooth_pairing">设备关联</string>
<string name="bluetooth_pairing_summary">打开 Android 的蓝牙设置关联设备</string>
<string name="sensor_options">感应器选项</string>
<!-- Bluetooth Sensor -->
<string name="bluetooth_connected">连接到 %s</string>
<string name="bluetooth_connecting">蓝牙感应器连接中 ...</string>
<string name="bluetooth_not_connected">未连接蓝牙感应器</string>
<!-- Sensor State -->
<string name="sensor_data_time">感应器数据时间</string>
<string name="power_meter_enabled">电源计量器已启用</string>
<string name="heart_rate_monitor_enabled">心率监视器已启用</string>
</resources>
+2 -2
View File
@@ -211,8 +211,8 @@ limitations under the License.
<string name="settings_auto_resume_track_timeout_summary">Maximum time in minutes after which a previously recording track will not be resumed on phone reboot</string>
<string name="settings_timestamp_track_name">Timestamp as track name</string>
<string name="settings_timestamp_track_name_on">The default track name will be the date and time at the start of the track</string>
<string name="settings_timestamp_track_name_off">Track names will have numbers which increment for each new track.</string>
<string name="settings_timestamp_track_name_summary_on">The default track name will be the date and time at the start of the track</string>
<string name="settings_timestamp_track_name_summary_off">Track names will have numbers which increment for each new track.</string>
<string name="pace_label">Pace</string>
+2 -2
View File
@@ -103,8 +103,8 @@ limitations under the License.
<CheckBoxPreference
android:key="@string/timestamp_track_name_key"
android:title="@string/settings_timestamp_track_name"
android:summaryOn="@string/settings_timestamp_track_name_on"
android:summaryOff="@string/settings_timestamp_track_name_off"
android:summaryOn="@string/settings_timestamp_track_name_summary_on"
android:summaryOff="@string/settings_timestamp_track_name_summary_off"
android:defaultValue="true" />
</PreferenceScreen>
@@ -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;
}
@@ -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
*/
@@ -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.
*/
@@ -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) {
@@ -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();
@@ -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());
}
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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() {
@@ -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();
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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) {
+3
View File
@@ -1,4 +1,7 @@
<factorypath>
<factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="WKSPJAR" id="/MyTracksTest/libs/AndroidMockGenerator.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="ANDROID_SDK/platforms/android-8/android.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="ANDROID_SDK/add-ons/addon_google_apis_google_inc_8/libs/maps.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="MYTRACKS_BIN" enabled="true" runInBatchMode="false"/>
</factorypath>
@@ -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
@@ -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());
}
}
@@ -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;
@@ -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<TrackRecordingService> {
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<Track> 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<Track> 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<Track> 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();
}
}
@@ -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());
@@ -75,4 +75,8 @@ public class ApiFeaturesTest extends TestCase {
features.setApiLevel(8);
assertTrue(features.hasModernSignalStrength());
}
public void testGetApiPlatformAdapter() {
assertNotNull(features.getApiPlatformAdapter());
}
}
@@ -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<String> existingFiles;