sync from main

This commit is contained in:
Jimmy Shih
2012-01-28 16:16:44 -08:00
53 changed files with 1802 additions and 863 deletions
+46 -5
View File
@@ -112,12 +112,19 @@ limitations under the License.
<meta-data android:name="com.google.android.backup.api_key" <meta-data android:name="com.google.android.backup.api_key"
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" /> android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<!-- Main track data provider -->
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider" <provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:authorities="com.google.android.maps.mytracks" android:authorities="com.google.android.maps.mytracks"
android:exported="true" android:exported="true"
android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA" android:readPermission="com.google.android.apps.mytracks.READ_TRACK_DATA"
android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" /> android:writePermission="com.google.android.apps.mytracks.WRITE_TRACK_DATA" />
<!-- Provider for search suggestions -->
<provider android:name="com.google.android.apps.mytracks.content.SearchEngineProvider"
android:authorities="com.google.android.maps.mytracks.search"
android:exported="false" />
<!-- Main activity -->
<activity android:name="com.google.android.apps.mytracks.MyTracks" <activity android:name="com.google.android.apps.mytracks.MyTracks"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale"> android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
<intent-filter> <intent-filter>
@@ -129,7 +136,12 @@ limitations under the License.
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.track" /> <data android:mimeType="vnd.android.cursor.item/vnd.google.track" />
<data android:mimeType="vnd.android.cursor.item/vnd.google.waypoint" />
</intent-filter> </intent-filter>
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity> </activity>
<activity android:name="com.google.android.apps.mytracks.MapActivity" <activity android:name="com.google.android.apps.mytracks.MapActivity"
@@ -137,13 +149,26 @@ limitations under the License.
<intent-filter> <intent-filter>
<category android:name="android.intent.category.TAB" /> <category android:name="android.intent.category.TAB" />
</intent-filter> </intent-filter>
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity> </activity>
<!-- TODO: VIEW and PICK for track cursor dir -->
<activity android:name="com.google.android.apps.mytracks.TrackList" /> <!-- TODO: Merge TrackList, WaypointsLIst and SearchActivity, then implement VIEW and PICK for track and waypoint cursor dir -->
<activity android:name="com.google.android.apps.mytracks.TrackList">
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity>
<!-- TODO: VIEW and PICK for waypoint cursor dir --> <!-- TODO: VIEW and PICK for waypoint cursor dir -->
<activity android:name="com.google.android.apps.mytracks.WaypointsList" /> <activity android:name="com.google.android.apps.mytracks.WaypointsList">
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity>
<activity android:name="com.google.android.apps.mytracks.TrackDetail" <activity android:name="com.google.android.apps.mytracks.TrackDetail"
android:label="@string/track_detail_title"> android:label="@string/track_detail_title">
@@ -168,7 +193,6 @@ limitations under the License.
<activity android:name="com.google.android.apps.mytracks.WaypointDetails" <activity android:name="com.google.android.apps.mytracks.WaypointDetails"
android:label="@string/marker_detail_title"> android:label="@string/marker_detail_title">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
@@ -191,6 +215,10 @@ limitations under the License.
<intent-filter> <intent-filter>
<category android:name="android.intent.category.TAB" /> <category android:name="android.intent.category.TAB" />
</intent-filter> </intent-filter>
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity> </activity>
<activity android:name="com.google.android.apps.mytracks.SensorStateActivity" <activity android:name="com.google.android.apps.mytracks.SensorStateActivity"
@@ -201,6 +229,10 @@ limitations under the License.
<intent-filter> <intent-filter>
<category android:name="android.intent.category.TAB" /> <category android:name="android.intent.category.TAB" />
</intent-filter> </intent-filter>
<!-- Enable searches via SearchActivity. -->
<meta-data android:name="android.app.default_searchable"
android:value="com.google.android.apps.mytracks.SearchActivity" />
</activity> </activity>
<activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.SendActivity" <activity android:name="com.google.android.apps.mytracks.io.sendtogoogle.SendActivity"
@@ -241,6 +273,15 @@ limitations under the License.
android:label="@string/welcome_title" android:label="@string/welcome_title"
android:exported="false" /> android:exported="false" />
<activity android:name="com.google.android.apps.mytracks.SearchActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
</intent-filter>
<meta-data android:name="android.app.searchable"
android:resource="@xml/search"/>
</activity>
<activity android:name="com.google.android.apps.mytracks.io.docs.SendDocsActivity"/> <activity android:name="com.google.android.apps.mytracks.io.docs.SendDocsActivity"/>
<activity android:name="com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity"/> <activity android:name="com.google.android.apps.mytracks.io.fusiontables.SendFusionTablesActivity"/>
<activity android:name="com.google.android.apps.mytracks.io.maps.ChooseMapActivity" <activity android:name="com.google.android.apps.mytracks.io.maps.ChooseMapActivity"
@@ -259,7 +300,7 @@ limitations under the License.
<service android:name="com.google.android.apps.mytracks.services.ControlRecordingService" <service android:name="com.google.android.apps.mytracks.services.ControlRecordingService"
android:exported="false" /> android:exported="false" />
<receiver android:name="com.google.android.apps.mytracks.BootReceiver"> <receiver android:name="com.google.android.apps.mytracks.BootReceiver">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" /> <action android:name="android.intent.action.BOOT_COMPLETED" />
+22 -3
View File
@@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2011 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/menu_record_track" <item android:id="@+id/menu_record_track"
android:icon="@drawable/ic_menu_recordtrack" android:icon="@drawable/ic_menu_recordtrack"
@@ -16,20 +31,24 @@
android:icon="@drawable/ic_menu_waypoints" android:icon="@drawable/ic_menu_waypoints"
android:title="@string/menu_markers"/> android:title="@string/menu_markers"/>
<item android:id="@+id/menu_search"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/menu_search"/>
<item android:id="@+id/menu_sensor_state" <item android:id="@+id/menu_sensor_state"
android:icon="@drawable/ic_menu_sensorstatus" android:icon="@drawable/ic_menu_sensorstatus"
android:orderInCategory="1000" android:orderInCategory="1000"
android:title="@string/menu_sensor_state"/> android:title="@string/menu_sensor_state"/>
<item android:id="@+id/menu_settings" <item android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences" android:icon="@android:drawable/ic_menu_preferences"
android:orderInCategory="1001" android:orderInCategory="1001"
android:title="@string/menu_settings"/> android:title="@string/menu_settings"/>
<item android:id="@+id/menu_aggregated_statistics" <item android:id="@+id/menu_aggregated_statistics"
android:orderInCategory="1002" android:orderInCategory="1002"
android:title="@string/menu_aggregated_statistics"/> android:title="@string/menu_aggregated_statistics"/>
<item android:id="@+id/menu_help" <item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_info_details" android:icon="@android:drawable/ic_menu_info_details"
android:orderInCategory="1003" android:orderInCategory="1003"
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2012 Google Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android" >
<item android:id="@+id/menu_search"
android:icon="@android:drawable/ic_menu_search"
android:title="@string/menu_search"
android:onClick="onSearch"
android:showAsAction="always" />
</menu>
+22 -7
View File
@@ -935,6 +935,10 @@ limitations under the License.
Screenshot: menu_record_track.png"> Screenshot: menu_record_track.png">
Tracks Tracks
</string> </string>
<string name="menu_search"
translation_description="In the menu, to start a textual search for tracks or waypoints.">
Search
</string>
<!-- My Location --> <!-- My Location -->
<string name="my_location_approximate_location" <string name="my_location_approximate_location"
@@ -1092,6 +1096,24 @@ limitations under the License.
Finished writing to the SD card. Finished writing to the SD card.
</string> </string>
<!-- Search interface -->
<string name="search_hint"
translation_description="The hint shown on the search box until the user types a search query.
See suggestion at http://developer.android.com/guide/topics/search/search-dialog.html about
keeping this consistent with other Android apps.">
Search My Tracks tracks and waypoints
</string>
<string name="search_global_description"
translation_description="Description of what is searched in My Tracks, to be displayed in the
global system search settings.">
My Tracks tracks and waypoints
</string>
<string name="search_voice_prompt"
translation_description="Prompt shown when the user activates voice search, asking him to
speak what will be searched for.">
Speak the name of a track or waypoint.
</string>
<!-- Send Google --> <!-- Send Google -->
<string name="send_google_by_my_tracks" <string name="send_google_by_my_tracks"
translation_description="When sending a track to Google, a note indicating it is created by translation_description="When sending a track to Google, a note indicating it is created by
@@ -1716,13 +1738,6 @@ limitations under the License.
Screenshot: settings_recording_top.png"> Screenshot: settings_recording_top.png">
Voice Voice
</string> </string>
<string name="settings_recording_voice_not_available"
translation_description="In the 'Recording' settings, the summary for the 'Voice'
explaining that this option is not available on your Android phone.
Screenshot: settings_recording_voice_not_available.png">
Voice announcement is not available on your Android phone.
</string>
<string name="settings_recording_voice_summary" <string name="settings_recording_voice_summary"
translation_description="In the 'Recording' settings, the summary for the 'Voice' translation_description="In the 'Recording' settings, the summary for the 'Voice'
dropdown option explaining that it configures the time or distance between two voice dropdown option explaining that it configures the time or distance between two voice
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<searchable xmlns:android="http://schemas.android.com/apk/res/android"
android:hint="@string/search_hint"
android:includeInGlobalSearch="true"
android:label="@string/my_tracks_app_name"
android:searchSettingsDescription="@string/search_global_description"
android:voicePromptText="@string/search_voice_prompt"
android:voiceSearchMode="showVoiceSearchButton|launchRecognizer"
android:searchSuggestAuthority="com.google.android.maps.mytracks.search"
android:searchSuggestThreshold="2"
android:searchSuggestSelection=" ? " />
@@ -26,10 +26,9 @@ import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType; import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -40,6 +39,7 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -116,9 +116,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
int volumeStream = setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_charts); setContentView(R.layout.mytracks_charts);
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -63,8 +63,7 @@ class CheckUnits {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE); Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = useMetricPreferences.edit(); SharedPreferences.Editor editor = useMetricPreferences.edit();
String key = context.getString(R.string.metric_units_key); String key = context.getString(R.string.metric_units_key);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
editor.putBoolean(key, metric));
} }
}); });
builder.setOnCancelListener(new DialogInterface.OnCancelListener() { builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -76,7 +75,7 @@ class CheckUnits {
} }
private static void recordCheckPerformed(SharedPreferences preferences) { private static void recordCheckPerformed(SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true)); preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
} }
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks; package com.google.android.apps.mytracks;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.AlertDialog; import android.app.AlertDialog;
@@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler {
Editor editor = prefs.edit(); Editor editor = prefs.edit();
// TODO: Go through data manager // TODO: Go through data manager
editor.putLong(context.getString(R.string.selected_track_key), -1); editor.putLong(context.getString(R.string.selected_track_key), -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
if (done != null) { if (done != null) {
Handler h = new Handler(); Handler h = new Handler();
h.post(done); h.post(done);
@@ -19,7 +19,7 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.UriUtils; import com.google.android.apps.mytracks.util.UriUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -110,7 +110,7 @@ public class DeleteTrack extends Activity
Constants.SETTINGS_NAME, Context.MODE_PRIVATE); Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (preferences.getLong(selectedKey, -1) == deleteTrackId) { if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
Editor editor = preferences.edit().putLong(selectedKey, -1); Editor editor = preferences.edit().putLong(selectedKey, -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
} }
} }
@@ -81,12 +81,12 @@ public class ExportAllTracks {
String exportFileFormat = activity.getString(R.string.track_list_export_file); String exportFileFormat = activity.getString(R.string.track_list_export_file);
String fileTypes[] = activity.getResources().getStringArray(R.array.file_types); String fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
String[] choices = new String[fileTypes.length]; String[] choices = new String[fileTypes.length];
for (int i = 0; i < fileTypes.length; i++) { for (int i = 0; i < fileTypes.length; i++) {
choices[i] = String.format(exportFileFormat, fileTypes[i]); choices[i] = String.format(exportFileFormat, fileTypes[i]);
} }
AlertDialog.Builder builder = new AlertDialog.Builder(activity); AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.track_list_export_all); builder.setTitle(R.string.track_list_export_all);
builder.setSingleChoiceItems(choices, 0, itemClick); builder.setSingleChoiceItems(choices, 0, itemClick);
@@ -160,7 +160,7 @@ public class ExportAllTracks {
try { try {
MyTracksProviderUtils providerUtils = MyTracksProviderUtils providerUtils =
MyTracksProviderUtils.Factory.get(activity); MyTracksProviderUtils.Factory.get(activity);
cursor = providerUtils.getTracksCursor(""); cursor = providerUtils.getTracksCursor(null, null, TracksColumns._ID);
if (cursor == null) { if (cursor == null) {
return; return;
} }
@@ -27,9 +27,8 @@ import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.file.SaveActivity; import com.google.android.apps.mytracks.io.file.SaveActivity;
import com.google.android.apps.mytracks.io.sendtogoogle.SendType; import com.google.android.apps.mytracks.io.sendtogoogle.SendType;
import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity; import com.google.android.apps.mytracks.io.sendtogoogle.UploadServiceChooserActivity;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect; import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint; import com.google.android.maps.GeoPoint;
@@ -44,6 +43,7 @@ import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.Parcelable; import android.os.Parcelable;
import android.provider.Settings; import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.ContextMenu; import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextMenu.ContextMenuInfo;
@@ -86,6 +86,25 @@ public class MapActivity extends com.google.android.maps.MapActivity
*/ */
private boolean keepMyLocationVisible; private boolean keepMyLocationVisible;
/**
* The ID of a track on which we want to show a waypoint.
* The waypoint will be shown as soon as the track is loaded.
*/
private long showWaypointTrackId;
/**
* The ID of a waypoint which we want to show.
* The waypoint will be shown as soon as its track is loaded.
*/
private long showWaypointId;
/**
* The track that's currently selected.
* This differs from {@link TrackDataHub#getSelectedTrackId} in that this one is only set after
* actual track data has been received.
*/
private long selectedTrackId;
/** /**
* The current pointer location. * The current pointer location.
* This is kept to quickly center on it when the user requests. * This is kept to quickly center on it when the user requests.
@@ -134,9 +153,7 @@ public class MapActivity extends com.google.android.maps.MapActivity
super.onCreate(bundle); super.onCreate(bundle);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
int volumeStream = setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -332,6 +349,47 @@ public class MapActivity extends com.google.android.maps.MapActivity
} }
} }
/**
* Zooms and pans the map so that the given waypoint is visible, when the given track is loaded.
* If the track is already loaded, it does that immediately.
*
* @param trackId the ID of the track on which to show the waypoint
* @param waypointId the ID of the waypoint to show
*/
public void showWaypoint(long trackId, long waypointId) {
synchronized (this) {
if (trackId == selectedTrackId) {
showWaypoint(waypointId);
return;
}
showWaypointTrackId = trackId;
showWaypointId = waypointId;
}
}
/**
* Does the proper zooming/panning for a just-loaded track.
* This may be either zooming to a waypoint that has been previously selected, or
* zooming to the whole track.
*
* @param track the loaded track
*/
private void zoomLoadedTrack(Track track) {
synchronized (this) {
if (track.getId() == showWaypointTrackId) {
// There's a waypoint to show in this track.
showWaypoint(showWaypointId);
showWaypointId = 0L;
showWaypointTrackId = 0L;
} else {
// Zoom out to show the whole track.
zoomMapToBoundaries(track);
}
}
}
@Override @Override
public void onSelectedTrackChanged(final Track track, final boolean isRecording) { public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
runOnUiThread(new Runnable() { runOnUiThread(new Runnable() {
@@ -345,7 +403,12 @@ public class MapActivity extends com.google.android.maps.MapActivity
if (trackSelected) { if (trackSelected) {
busyPane.setVisibility(View.VISIBLE); busyPane.setVisibility(View.VISIBLE);
zoomMapToBoundaries(track); synchronized (this) {
// Need to get the track ID only at this point, to prevent a race condition
// among showWaypoint, zoomLoadedTrack and dataHub.loadTrack.
selectedTrackId = track.getId();
zoomLoadedTrack(track);
}
mapOverlay.setShowEndMarker(!isRecording); mapOverlay.setShowEndMarker(!isRecording);
busyPane.setVisibility(View.GONE); busyPane.setVisibility(View.GONE);
@@ -37,6 +37,8 @@ class MenuManager {
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
activity.getMenuInflater().inflate(R.menu.main, menu); activity.getMenuInflater().inflate(R.menu.main, menu);
// TODO: Replace search button with search widget if API level >= 11
return true; return true;
} }
@@ -85,6 +87,10 @@ class MenuManager {
case R.id.menu_help: { case R.id.menu_help: {
return startActivity(WelcomeActivity.class); return startActivity(WelcomeActivity.class);
} }
case R.id.menu_search: {
// TODO: Pass the current track ID and current location to do some fancier ranking.
activity.onSearchRequested();
}
} }
return false; return false;
} }
@@ -21,13 +21,15 @@ import com.google.android.apps.analytics.GoogleAnalyticsTracker;
import com.google.android.apps.mytracks.content.MyTracksProviderUtils; import com.google.android.apps.mytracks.content.MyTracksProviderUtils;
import com.google.android.apps.mytracks.content.TrackDataHub; import com.google.android.apps.mytracks.content.TrackDataHub;
import com.google.android.apps.mytracks.content.TracksColumns; import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.apps.mytracks.io.file.TempFileCleaner; import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService; import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection; import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtil; import com.google.android.apps.mytracks.util.EulaUtil;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UriUtils; import com.google.android.apps.mytracks.util.UriUtils;
@@ -46,6 +48,7 @@ import android.content.res.Resources;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@@ -67,7 +70,7 @@ import android.widget.Toast;
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
public class MyTracks extends TabActivity implements OnTouchListener { public class MyTracks extends TabActivity implements OnTouchListener {
private static final int DIALOG_EULA_ID = 0; private static final int DIALOG_EULA_ID = 0;
private TrackDataHub dataHub; private TrackDataHub dataHub;
/** /**
@@ -139,9 +142,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "MyTracks.onCreate"); Log.d(TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
if (!SystemUtils.isRelease(this)) { if (!SystemUtils.isRelease(this)) {
apiFeatures.getApiAdapter().enableStrictMode(); ApiAdapterFactory.getApiAdapter().enableStrictMode();
} }
tracker = GoogleAnalyticsTracker.getInstance(); tracker = GoogleAnalyticsTracker.getInstance();
@@ -157,14 +159,14 @@ public class MyTracks extends TabActivity implements OnTouchListener {
menuManager = new MenuManager(this); menuManager = new MenuManager(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback); serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
// If the user just starts typing (on a device with a keyboard), we start a search.
setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL);
final Resources res = getResources(); final Resources res = getResources();
final TabHost tabHost = getTabHost(); final TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1") tabHost.addTab(tabHost.newTabSpec("tab1")
@@ -199,7 +201,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
showDialog(DIALOG_EULA_ID); showDialog(DIALOG_EULA_ID);
} }
} }
@Override @Override
protected void onStart() { protected void onStart() {
Log.d(TAG, "MyTracks.onStart"); Log.d(TAG, "MyTracks.onStart");
@@ -219,6 +221,19 @@ public class MyTracks extends TabActivity implements OnTouchListener {
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) { && UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data); long trackId = ContentUris.parseId(data);
dataHub.loadTrack(trackId); dataHub.loadTrack(trackId);
} else if (Intent.ACTION_VIEW.equals(action)
&& WaypointsColumns.CONTENT_ITEMTYPE.equals(intent.getType())
&& UriUtils.matchesContentUri(data, WaypointsColumns.CONTENT_URI)) {
// TODO(rdamazio): Waypoint URIs should be base/trackid/waypointid
long waypointId = ContentUris.parseId(data);
Waypoint waypoint = providerUtils.getWaypoint(waypointId);
long trackId = waypoint.getTrackId();
// Request that the waypoint is shown (now or when the right track is loaded).
showWaypoint(trackId, waypointId);
// Load the right track, if not loaded already.
dataHub.loadTrack(trackId);
} }
} }
@@ -293,7 +308,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
return null; return null;
} }
} }
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu); super.onCreateOptionsMenu(menu);
@@ -366,12 +381,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
if (results != null) { if (results != null) {
final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1); final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1);
if (waypointId >= 0) { if (waypointId >= 0) {
MapActivity map = showWaypoint(trackId, waypointId);
(MapActivity) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(waypointId);
}
} }
} }
break; break;
@@ -387,6 +397,17 @@ public class MyTracks extends TabActivity implements OnTouchListener {
} }
} }
private void showWaypoint(long trackId, long waypointId) {
MapActivity map =
(MapActivity) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(trackId, waypointId);
} else {
Log.e(TAG, "Couldnt' get map tab");
}
}
@Override @Override
public boolean onTouch(View v, MotionEvent event) { public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) { if (event.getAction() == MotionEvent.ACTION_DOWN) {
@@ -0,0 +1,218 @@
/*
* Copyright 2011 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks;
import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.content.SearchEngine;
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
import com.google.android.apps.mytracks.content.SearchEngineProvider;
import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.TracksColumns;
import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointsColumns;
import com.google.android.maps.mytracks.R;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.ContentUris;
import android.content.Intent;
import android.location.Location;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import android.view.View;
import android.widget.ListView;
import android.widget.SimpleAdapter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.SortedSet;
/**
* Activity to search for tracks or waypoints.
*
* @author Rodrigo Damazio
*/
public class SearchActivity extends ListActivity {
private static final String EXTRA_CURRENT_TRACK_ID = "trackId";
private static final boolean LOG_SCORES = true;
private SearchEngine engine;
private LocationManager locationManager;
private SearchRecentSuggestions suggestions;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
engine = new SearchEngine(this);
suggestions = SearchEngineProvider.newHelper(this);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
handleIntent(getIntent());
}
@Override
protected void onNewIntent(Intent intent) {
setIntent(intent);
handleIntent(intent);
}
private void handleIntent(Intent intent) {
if (!Intent.ACTION_SEARCH.equals(intent.getAction())) {
Log.e(TAG, "Got bad search intent: " + intent);
finish();
return;
}
String textQuery = intent.getStringExtra(SearchManager.QUERY);
Location currentLocation = locationManager.getLastKnownLocation("gps");
long currentTrackId = intent.getLongExtra(EXTRA_CURRENT_TRACK_ID, -1);
long currentTimestamp = System.currentTimeMillis();
final SearchQuery query =
new SearchQuery(textQuery, currentLocation, currentTrackId, currentTimestamp);
// Do the actual search in a separate thread.
new Thread() {
@Override
public void run() {
doSearch(query);
}
}.start();
}
private void doSearch(SearchQuery query) {
SortedSet<ScoredResult> scoredResults = engine.search(query);
final List<? extends Map<String, ?>> displayResults = prepareResultsforDisplay(scoredResults);
if (LOG_SCORES) {
Log.i(TAG, "Search scores: " + displayResults);
}
// Then go back to the UI thread to display them.
runOnUiThread(new Runnable() {
@Override
public void run() {
showSearchResults(displayResults);
}
});
// Save the query as a suggestion for the future.
suggestions.saveRecentQuery(query.textQuery, null);
}
private List<Map<String, Object>> prepareResultsforDisplay(
Collection<ScoredResult> scoredResults) {
ArrayList<Map<String, Object>> output = new ArrayList<Map<String, Object>>(scoredResults.size());
for (ScoredResult result : scoredResults) {
Map<String, Object> resultMap = new HashMap<String, Object>();
if (result.track != null) {
prepareTrackForDisplay(result.track, resultMap);
} else {
prepareWaypointForDisplay(result.waypoint, resultMap);
}
resultMap.put("score", result.score);
output.add(resultMap);
}
return output;
}
private void prepareWaypointForDisplay(Waypoint waypoint, Map<String, Object> resultMap) {
// TODO: Yellow pushpin for statistics marker.
resultMap.put("icon", R.drawable.blue_pushpin);
resultMap.put("name", waypoint.getName());
resultMap.put("description", waypoint.getDescription());
resultMap.put("category", waypoint.getCategory());
resultMap.put("time", String.format("%tc", waypoint.getLocation().getTime()));
resultMap.put("trackId", waypoint.getTrackId());
resultMap.put("waypointId", waypoint.getId());
}
private void prepareTrackForDisplay(Track track, Map<String, Object> resultMap) {
resultMap.put("icon", R.drawable.track);
resultMap.put("name", track.getName());
resultMap.put("description", track.getDescription());
resultMap.put("category", track.getCategory());
resultMap.put("time", String.format("%tc", track.getStatistics().getStartTime()));
resultMap.put("trackId", track.getId());
}
/**
* Shows the given search results.
* Must be run from the UI thread.
* @param data
*
* @param scoredResults the results to show, properly ordered
*/
private void showSearchResults(List<? extends Map<String, ?>> data) {
SimpleAdapter adapter = new SimpleAdapter(this, data,
// TODO: Custom view for search results.
R.layout.mytracks_list_item,
new String[] {
"icon",
"name",
"description",
"category",
"time"
},
new int[] {
R.id.trackdetails_item_icon,
R.id.trackdetails_item_name,
R.id.trackdetails_item_description,
R.id.trackdetails_item_category,
R.id.trackdetails_item_time
});
setListAdapter(adapter);
}
@Override
protected void onListItemClick(ListView l, View v, int position, long id) {
@SuppressWarnings("unchecked")
Map<String, Object> clickedData = (Map<String, Object>) getListAdapter().getItem(position);
startActivity(createViewDataIntent(clickedData));
}
private Intent createViewDataIntent(Map<String, Object> clickedData) {
Intent intent = new Intent(Intent.ACTION_VIEW);
if (clickedData.containsKey("waypointId")) {
long waypointId = (Long) clickedData.get("waypointId");
Uri uri = ContentUris.withAppendedId(WaypointsColumns.CONTENT_URI, waypointId);
intent.setDataAndType(uri, WaypointsColumns.CONTENT_ITEMTYPE);
} else {
long trackId = (Long) clickedData.get("trackId");
Uri uri = ContentUris.withAppendedId(TracksColumns.CONTENT_URI, trackId);
intent.setDataAndType(uri, TracksColumns.CONTENT_ITEMTYPE);
}
return intent;
}
}
@@ -20,13 +20,13 @@ import static com.google.android.apps.mytracks.Constants.TAG;
import com.google.android.apps.mytracks.io.backup.BackupActivityHelper; import com.google.android.apps.mytracks.io.backup.BackupActivityHelper;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.ant.AntUtils; import com.google.android.apps.mytracks.services.sensors.ant.AntUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils; import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.apps.mytracks.util.UnitConversions; import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener; import android.content.DialogInterface.OnClickListener;
import android.content.Intent; import android.content.Intent;
@@ -44,6 +44,7 @@ import android.preference.PreferenceCategory;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.preference.PreferenceScreen; import android.preference.PreferenceScreen;
import android.provider.Settings; import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log; import android.util.Log;
import android.widget.Toast; import android.widget.Toast;
@@ -102,10 +103,7 @@ public class SettingsActivity extends PreferenceActivity {
super.onCreate(icicle); super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance(); setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
// Tell it where to read/write preferences // Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager(); PreferenceManager preferenceManager = getPreferenceManager();
@@ -113,23 +111,13 @@ public class SettingsActivity extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0); preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup // Set up automatic preferences backup
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this); backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
preferences = preferenceManager.getSharedPreferences(); preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener); preferences.registerOnSharedPreferenceChangeListener(backupListener);
// Load the preferences to be displayed // Load the preferences to be displayed
addPreferencesFromResource(R.xml.preferences); addPreferencesFromResource(R.xml.preferences);
// Disable voice announcement if not available
if (!apiFeatures.hasTextToSpeech()) {
IntegerListPreference announcementFrequency =
(IntegerListPreference) findPreference(
getString(R.string.announcement_frequency_key));
announcementFrequency.setEnabled(false);
announcementFrequency.setValue(TASK_FREQUENCY_OFF);
announcementFrequency.setSummary(R.string.settings_recording_voice_not_available);
}
setRecordingIntervalOptions(); setRecordingIntervalOptions();
setAutoResumeTimeoutOptions(); setAutoResumeTimeoutOptions();
@@ -557,20 +545,17 @@ public class SettingsActivity extends PreferenceActivity {
* Configures preference actions related to bluetooth. * Configures preference actions related to bluetooth.
*/ */
private void configureBluetoothPreferences() { private void configureBluetoothPreferences() {
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) { // Populate the list of bluetooth devices
// Populate the list of bluetooth devices populateBluetoothDeviceList();
populateBluetoothDeviceList(); // Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener(
// Make the pair devices preference go to the system preferences new OnPreferenceClickListener() {
findPreference(getString(R.string.bluetooth_pairing_key)) public boolean onPreferenceClick(Preference preference) {
.setOnPreferenceClickListener(new OnPreferenceClickListener() { Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
public boolean onPreferenceClick(Preference preference) { startActivity(settingsIntent);
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); return false;
startActivity(settingsIntent); }
return false; });
}
});
}
} }
/** /**
@@ -582,7 +567,10 @@ public class SettingsActivity extends PreferenceActivity {
List<String> entryValues = new ArrayList<String>(); List<String> entryValues = new ArrayList<String>();
// The actual devices // The actual devices
BluetoothDeviceUtils.getInstance().populateDeviceLists(entries, entryValues); BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter != null) {
BluetoothDeviceUtils.populateDeviceLists(bluetoothAdapter, entries, entryValues);
}
CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]); CharSequence[] entriesArray = entries.toArray(new CharSequence[entries.size()]);
CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]); CharSequence[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
@@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity {
SharedPreferences prefs = getPreferenceManager().getSharedPreferences(); SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
Editor editor = prefs.edit(); Editor editor = prefs.edit();
editor.putString(getString(id), metricspeed); editor.putString(getString(id), metricspeed);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
/** /**
@@ -23,8 +23,7 @@ import com.google.android.apps.mytracks.content.TrackDataHub.ListenerDataType;
import com.google.android.apps.mytracks.content.TrackDataListener; import com.google.android.apps.mytracks.content.TrackDataListener;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.services.ServiceUtils; import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -32,6 +31,7 @@ import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.view.Window; import android.view.Window;
@@ -105,9 +105,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils = new StatsUtilities(this); utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume // The volume we want to control is the Text-To-Speech volume
int volumeStream = setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
// We don't need a window title bar: // We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE); requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -286,6 +286,17 @@ public class TrackList extends ListActivity
super.onDestroy(); super.onDestroy();
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_only, menu);
return true;
}
/* Callback from menu/search_only.xml */
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
onSearchRequested();
}
private void updateButtonsEnabled() { private void updateButtonsEnabled() {
View deleteAll = findViewById(R.id.tracklist_btn_delete_all); View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all); View exportAll = findViewById(R.id.tracklist_btn_export_all);
@@ -198,6 +198,17 @@ public class WaypointsList extends ListActivity
super.onDestroy(); super.onDestroy();
} }
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.search_only, menu);
return true;
}
/* Callback from menu/search_only.xml */
public void onSearch(@SuppressWarnings("unused") MenuItem i) {
onSearchRequested();
}
@Override @Override
public void onClick(View v) { public void onClick(View v) {
WaypointCreationRequest request; WaypointCreationRequest request;
@@ -266,10 +277,10 @@ public class WaypointsList extends ListActivity
if (columnIndex == timeIdx) { if (columnIndex == timeIdx) {
long time = cursor.getLong(timeIdx); long time = cursor.getLong(timeIdx);
TextView textView = (TextView) view; TextView textView = (TextView) view;
if (time == 0) { if (time == 0) {
textView.setVisibility(View.GONE); textView.setVisibility(View.GONE);
} else { } else {
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time)); textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
textView.setVisibility(View.VISIBLE); textView.setVisibility(View.VISIBLE);
} }
@@ -16,7 +16,6 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.ContentProvider; import android.content.ContentProvider;
@@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider {
} else { } else {
throw new IllegalArgumentException("Unknown URL " + url); throw new IllegalArgumentException("Unknown URL " + url);
} }
Log.i(Constants.TAG, "Build query: "
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) { + qb.buildQuery(projection, selection, selectionArgs, null, null, sortOrder, null));
Log.i(Constants.TAG,
"Build query: " + qb.buildQuery(projection, selection, selectionArgs,
null, null, sortOrder, null));
}
Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, Cursor c = qb.query(db, projection, selection, selectionArgs, null, null,
sortOrder); sortOrder);
c.setNotificationUri(getContext().getContentResolver(), url); c.setNotificationUri(getContext().getContentResolver(), url);
@@ -0,0 +1,405 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.content.Context;
import android.database.Cursor;
import android.location.Location;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.SortedSet;
import java.util.TreeSet;
/**
* Engine for searching for tracks and waypoints by text.
*
* @author Rodrigo Damazio
*/
public class SearchEngine {
/** WHERE query to get tracks by name. */
private static final String TRACK_SELECTION_QUERY =
TracksColumns.NAME + " LIKE ? OR " +
TracksColumns.DESCRIPTION + " LIKE ? OR " +
TracksColumns.CATEGORY + " LIKE ?";
/** WHERE query to get waypoints by name. */
private static final String WAYPOINT_SELECTION_QUERY =
WaypointsColumns.NAME + " LIKE ? OR " +
WaypointsColumns.DESCRIPTION + " LIKE ? OR " +
WaypointsColumns.CATEGORY + " LIKE ?";
/** Order of track results. */
private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000";
/** Order of waypoint results. */
private static final String WAYPOINT_SELECTION_ORDER = WaypointsColumns._ID + " DESC";
/** How much we promote a match in the track category. */
private static final double TRACK_CATEGORY_PROMOTION = 2.0;
/** How much we promote a match in the track description. */
private static final double TRACK_DESCRIPTION_PROMOTION = 8.0;
/** How much we promote a match in the track name. */
private static final double TRACK_NAME_PROMOTION = 16.0;
/** How much we promote a waypoint result if it's in the currently-selected track. */
private static final double CURRENT_TRACK_WAYPOINT_PROMOTION = 2.0;
/** How much we promote a track result if it's the currently-selected track. */
private static final double CURRENT_TRACK_DEMOTION = 0.5;
/** Maximum number of waypoints which will be retrieved and scored. */
private static final int MAX_SCORED_WAYPOINTS = 100;
/** Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) */
private static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L;
/**
* Description of a search query, along with all contextual data needed to execute it.
*/
public static class SearchQuery {
public SearchQuery(String textQuery, Location currentLocation, long currentTrackId,
long currentTimestamp) {
this.textQuery = textQuery.toLowerCase();
this.currentLocation = currentLocation;
this.currentTrackId = currentTrackId;
this.currentTimestamp = currentTimestamp;
}
public final String textQuery;
public final Location currentLocation;
public final long currentTrackId;
public final long currentTimestamp;
}
/**
* Description of a search result which has been retrieved and scored.
*/
public static class ScoredResult {
ScoredResult(Track track, double score) {
this.track = track;
this.waypoint = null;
this.score = score;
}
ScoredResult(Waypoint waypoint, double score) {
this.track = null;
this.waypoint = waypoint;
this.score = score;
}
public final Track track;
public final Waypoint waypoint;
public final double score;
@Override
public String toString() {
return "ScoredResult ["
+ (track != null ? ("trackId=" + track.getId() + ", ") : "")
+ (waypoint != null ? ("wptId=" + waypoint.getId() + ", ") : "")
+ "score=" + score + "]";
}
}
/** Comparador for scored results. */
private static final Comparator<ScoredResult> SCORED_RESULT_COMPARATOR =
new Comparator<ScoredResult>() {
@Override
public int compare(ScoredResult r1, ScoredResult r2) {
// Score ordering.
int scoreDiff = Double.compare(r2.score, r1.score);
if (scoreDiff != 0) {
return scoreDiff;
}
// Make tracks come before waypoints.
if (r1.waypoint != null && r2.track != null) {
return 1;
} else if (r1.track != null && r2.waypoint != null) {
return -1;
}
// Finally, use arbitrary ordering, by ID.
long id1 = r1.track != null ? r1.track.getId() : r1.waypoint.getId();
long id2 = r2.track != null ? r2.track.getId() : r2.waypoint.getId();
long idDiff = id2 - id1;
return Long.signum(idDiff);
}
};
private final MyTracksProviderUtils providerUtils;
public SearchEngine(Context ctx) {
providerUtils = MyTracksProviderUtils.Factory.get(ctx);
}
public SearchEngine(MyTracksProviderUtils providerUtils) {
this.providerUtils = providerUtils;
}
/**
* Executes a search query and returns a set of sorted results.
*
* @param query the query to execute
* @return a set of results, sorted according to their score
*/
public SortedSet<ScoredResult> search(SearchQuery query) {
ArrayList<Track> tracks = new ArrayList<Track>();
ArrayList<Waypoint> waypoints = new ArrayList<Waypoint>();
TreeSet<ScoredResult> scoredResults = new TreeSet<ScoredResult>(SCORED_RESULT_COMPARATOR);
retrieveTracks(query, tracks);
retrieveWaypoints(query, waypoints);
scoreTrackResults(tracks, query, scoredResults);
scoreWaypointResults(waypoints, query, scoredResults);
return scoredResults;
}
/**
* Retrieves tracks matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting tracks
*/
private void retrieveTracks(SearchQuery query, ArrayList<Track> tracks) {
String queryLikeSelection = "%" + query.textQuery + "%";
String[] trackSelectionArgs = new String[] {
queryLikeSelection,
queryLikeSelection,
queryLikeSelection };
Cursor tracksCursor = providerUtils.getTracksCursor(
TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER);
if (tracksCursor != null) {
try {
tracks.ensureCapacity(tracksCursor.getCount());
while (tracksCursor.moveToNext()) {
tracks.add(providerUtils.createTrack(tracksCursor));
}
} finally {
tracksCursor.close();
}
}
}
/**
* Retrieves waypoints matching the given query from the database.
*
* @param query the query to retrieve for
* @param tracks list to fill with the resulting waypoints
*/
private void retrieveWaypoints(SearchQuery query, ArrayList<Waypoint> waypoints) {
String queryLikeSelection2 = "%" + query.textQuery + "%";
String[] waypointSelectionArgs = new String[] {
queryLikeSelection2,
queryLikeSelection2,
queryLikeSelection2 };
Cursor waypointsCursor = providerUtils.getWaypointsCursor(
WAYPOINT_SELECTION_QUERY, waypointSelectionArgs, WAYPOINT_SELECTION_ORDER,
MAX_SCORED_WAYPOINTS);
if (waypointsCursor != null) {
try {
waypoints.ensureCapacity(waypointsCursor.getCount());
while (waypointsCursor.moveToNext()) {
waypoints.add(providerUtils.createWaypoint(waypointsCursor));
}
} finally {
waypointsCursor.close();
}
}
}
/**
* Scores a collection of track results.
*
* @param tracks the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreTrackResults(Collection<Track> tracks, SearchQuery query, Collection<ScoredResult> output) {
for (Track track : tracks) {
// Calculate the score.
double score = scoreTrackResult(query, track);
// Add to the output.
output.add(new ScoredResult(track, score));
}
}
/**
* Scores a single track result.
*
* @param query the query to score for
* @param track the results to score
* @return the score for the track
*/
private double scoreTrackResult(SearchQuery query, Track track) {
double score = 1.0;
score *= getTitleBoost(query, track.getName(), track.getDescription(), track.getCategory());
TripStatistics statistics = track.getStatistics();
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, statistics.getMeanLatitude(), statistics.getMeanLongitude());
long meanTimestamp = (statistics.getStartTime() + statistics.getStopTime()) / 2L;
score *= getTimeBoost(query, meanTimestamp);
// Score the currently-selected track lower (user is already there, wouldn't be searching for it).
if (track.getId() == query.currentTrackId) {
score *= CURRENT_TRACK_DEMOTION;
}
return score;
}
/**
* Scores a collection of waypoint results.
*
* @param waypoints the results to score
* @param query the query to score for
* @param output the collection to fill with scored results
*/
private void scoreWaypointResults(Collection<Waypoint> waypoints, SearchQuery query, Collection<ScoredResult> output) {
for (Waypoint waypoint : waypoints) {
// Calculate the score.
double score = scoreWaypointResult(query, waypoint);
// Add to the output.
output.add(new ScoredResult(waypoint, score));
}
}
/**
* Scores a single waypoint result.
*
* @param query the query to score for
* @param waypoint the results to score
* @return the score for the waypoint
*/
private double scoreWaypointResult(SearchQuery query, Waypoint waypoint) {
double score = 1.0;
Location location = waypoint.getLocation();
score *= getTitleBoost(query, waypoint.getName(), waypoint.getDescription(), waypoint.getCategory());
// TODO: Also boost for proximity to the currently-centered position on the map.
score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude());
score *= getTimeBoost(query, location.getTime());
// Score waypoints in the currently-selected track higher (searching inside the current track).
if (query.currentTrackId != -1 && waypoint.getTrackId() == query.currentTrackId) {
score *= CURRENT_TRACK_WAYPOINT_PROMOTION;
}
return score;
}
/**
* Calculates the boosting of the score due to the field(s) in which the match occured.
*
* @param query the query to boost for
* @param name the name of the track or waypoint
* @param description the description of the track or waypoint
* @param category the category of the track or waypoint
* @return the total boost to be applied to the result
*/
private double getTitleBoost(SearchQuery query,
String name, String description, String category) {
// Title boost: track name > description > category.
double boost = 1.0;
if (name.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_NAME_PROMOTION;
}
if (description.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_DESCRIPTION_PROMOTION;
}
if (category.toLowerCase().contains(query.textQuery)) {
boost *= TRACK_CATEGORY_PROMOTION;
}
return boost;
}
/**
* Calculates the boosting of the score due to the recency of the matched entity.
*
* @param query the query to boost for
* @param timestamp the timestamp to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getTimeBoost(SearchQuery query, long timestamp) {
if (timestamp < OLDEST_ALLOWED_TIMESTAMP) {
// Safety: if timestamp is too old or invalid, don't rank based on time.
return 1.0;
}
// Score recent tracks higher.
long timeAgoHours = (query.currentTimestamp - timestamp) / (60L * 60L * 1000L);
if (timeAgoHours > 0L) {
return squash(timeAgoHours);
} else {
// Should rarely happen (track recorded in the last hour).
return Double.POSITIVE_INFINITY;
}
}
/**
* Calculates the boosting of the score due to proximity to a location.
*
* @param query the query to boost for
* @param latitude the latitude to calculate the boost for
* @param longitude the longitude to calculate the boost for
* @return the total boost to be applied to the result
*/
private double getDistanceBoost(SearchQuery query, double latitude, double longitude) {
if (query.currentLocation == null) {
return 1.0;
}
float[] distanceResults = new float[1];
Location.distanceBetween(
latitude, longitude,
query.currentLocation.getLatitude(), query.currentLocation.getLongitude(),
distanceResults);
// Score tracks close to the current location higher.
double distanceKm = distanceResults[0] / 1000.0;
if (distanceKm > 0.0) {
// Use the inverse of the amortized distance.
return squash(distanceKm);
} else {
// Should rarely happen (distance is exactly 0).
return Double.POSITIVE_INFINITY;
}
}
/**
* Squashes a number by calculating 1 / log (1 + x).
*/
private static double squash(double x) {
return 1.0 / Math.log1p(x);
}
}
@@ -0,0 +1,44 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.content;
import android.content.Context;
import android.content.SearchRecentSuggestionsProvider;
import android.provider.SearchRecentSuggestions;
/**
* Content provider for search suggestions.
*
* @author Rodrigo Damazio
*/
public class SearchEngineProvider extends SearchRecentSuggestionsProvider {
private static final String AUTHORITY = "com.google.android.maps.mytracks.search";
private static final int MODE = DATABASE_MODE_QUERIES;
public SearchEngineProvider() {
setupSuggestions(AUTHORITY, MODE);
}
// TODO: Also add suggestions from the database.
/**
* Creates and returns a helper for adding recent queries or clearing the recent query history.
*/
public static SearchRecentSuggestions newHelper(Context context) {
return new SearchRecentSuggestions(context, AUTHORITY, MODE);
}
}
@@ -28,7 +28,7 @@ import com.google.android.apps.mytracks.content.MyTracksProviderUtils.DoubleBuff
import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator; import com.google.android.apps.mytracks.content.MyTracksProviderUtils.LocationIterator;
import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState; import com.google.android.apps.mytracks.content.TrackDataListener.ProviderState;
import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration; import com.google.android.apps.mytracks.content.TrackDataListeners.ListenerRegistration;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -196,7 +196,7 @@ public class TrackDataHub {
private DoubleBufferedLocationFactory locationFactory; private DoubleBufferedLocationFactory locationFactory;
private boolean started = false; private boolean started = false;
/** /**
* Builds a new {@link TrackDataHub} instance. * Builds a new {@link TrackDataHub} instance.
*/ */
@@ -274,7 +274,7 @@ public class TrackDataHub {
Log.w(TAG, "Not started, ignoring"); Log.w(TAG, "Not started, ignoring");
return; return;
} }
// Unregister internal listeners even if there are external listeners registered. // Unregister internal listeners even if there are external listeners registered.
dataSourceManager.unregisterAllListeners(); dataSourceManager.unregisterAllListeners();
listenerHandlerThread.getLooper().quit(); listenerHandlerThread.getLooper().quit();
@@ -399,7 +399,7 @@ public class TrackDataHub {
// Save the selection to memory and flush. // Save the selection to memory and flush.
selectedTrackId = trackId; selectedTrackId = trackId;
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges( ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId)); preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
// Force it to reload data from the beginning. // Force it to reload data from the beginning.
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.backup; package com.google.android.apps.mytracks.io.backup;
import com.google.android.apps.mytracks.content.ContentTypeIds; import com.google.android.apps.mytracks.content.ContentTypeIds;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
@@ -106,7 +106,7 @@ class PreferenceBackupHelper {
byte typeId = reader.readByte(); byte typeId = reader.readByte();
readAndSetPreference(name, typeId, reader, editor); readAndSetPreference(name, typeId, reader, editor);
} }
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
/** /**
@@ -8,7 +8,7 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils; import com.google.android.apps.mytracks.io.sendtogoogle.SendToGoogleUtils;
import com.google.android.apps.mytracks.stats.DoubleBuffer; import com.google.android.apps.mytracks.stats.DoubleBuffer;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
@@ -109,7 +109,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask<Void, Integer, Boolean>
context = activity.getApplicationContext(); context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context); myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport(); HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride()); httpRequestFactory = transport.createRequestFactory(new MethodOverride());
canRetry = true; canRetry = true;
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle; package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.accounts.Account; import android.accounts.Account;
@@ -153,7 +153,7 @@ public class AccountChooserActivity extends Activity {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE); Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit(); Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name); editor.putString(getString(R.string.preferred_account_key), account.name);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
Intent intent = new Intent(); Intent intent = new Intent();
intent.putExtra(ACCOUNT, account); intent.putExtra(ACCOUNT, account);
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle; package com.google.android.apps.mytracks.io.sendtogoogle;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.app.Activity; import android.app.Activity;
@@ -218,7 +218,7 @@ public class UploadServiceChooserActivity extends Activity {
editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables()); editor.putBoolean(getString(R.string.send_to_fusion_tables_key), sendFusionTables());
editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs()); editor.putBoolean(getString(R.string.send_to_docs_key), sendDocs());
} }
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
/** /**
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services; package com.google.android.apps.mytracks.services;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
@@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
public void setAutoResumeTrackCurrentRetry(int retryAttempts) { public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts); editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
public void setRecordingTrack(long id) { public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id); editor.putLong(recordingTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
public void setSelectedTrack(long id) { public void setSelectedTrack(long id) {
Editor editor = sharedPreferences.edit(); Editor editor = sharedPreferences.edit();
editor.putLong(selectedTrackKey, id); editor.putLong(selectedTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
public void shutdown() { public void shutdown() {
@@ -36,14 +36,12 @@ import com.google.android.apps.mytracks.services.tasks.SplitTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder; import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
import com.google.android.apps.mytracks.util.LocationUtils; import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils; import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.Service; import android.app.Service;
import android.content.ContentValues; import android.content.ContentValues;
@@ -80,7 +78,6 @@ public class TrackRecordingService extends Service {
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; private LocationManager locationManager;
private WakeLock wakeLock; private WakeLock wakeLock;
@@ -243,8 +240,6 @@ public class TrackRecordingService extends Service {
super.onCreate(); super.onCreate();
Log.d(TAG, "TrackRecordingService.onCreate"); Log.d(TAG, "TrackRecordingService.onCreate");
providerUtils = MyTracksProviderUtils.Factory.get(this); providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE); locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
setUpTaskExecutors(); setUpTaskExecutors();
@@ -369,7 +364,6 @@ public class TrackRecordingService extends Service {
// Make sure we have no indirect references to this service. // Make sure we have no indirect references to this service.
locationManager = null; locationManager = null;
notificationManager = null;
providerUtils = null; providerUtils = null;
binder.detachFromService(); binder.detachFromService();
binder = null; binder = null;
@@ -485,8 +479,6 @@ public class TrackRecordingService extends Service {
* Shows the notification message and icon in the notification bar. * Shows the notification message and icon in the notification bar.
*/ */
private void showNotification() { private void showNotification() {
final ApiLevelAdapter apiLevelAdapter =
ApiFeatures.getInstance().getApiAdapter();
if (isRecording) { if (isRecording) {
Notification notification = new Notification( Notification notification = new Notification(
R.drawable.arrow_320, null /* tickerText */, R.drawable.arrow_320, null /* tickerText */,
@@ -497,16 +489,24 @@ public class TrackRecordingService extends Service {
notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name), notification.setLatestEventInfo(this, getString(R.string.my_tracks_app_name),
getString(R.string.track_record_notification), contentIntent); getString(R.string.track_record_notification), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR; notification.flags += Notification.FLAG_NO_CLEAR;
apiLevelAdapter.startForeground(this, notificationManager, 1, startForegroundService(notification);
notification);
} else { } else {
apiLevelAdapter.stopForeground(this, notificationManager, 1); stopForegroundService();
} }
} }
@VisibleForTesting
protected void startForegroundService(Notification notification) {
startForeground(1, notification);
}
@VisibleForTesting
protected void stopForegroundService() {
stopForeground(true);
}
private void setUpTaskExecutors() { private void setUpTaskExecutors() {
announcementExecutor = new PeriodicTaskExecutor( announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory()); splitExecutor = new PeriodicTaskExecutor(this, new SplitTask.Factory());
} }
@@ -18,7 +18,7 @@ package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
@@ -31,8 +31,6 @@ import android.util.Log;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID; import java.util.UUID;
/** /**
@@ -44,8 +42,9 @@ import java.util.UUID;
* @author Sandor Dornbush * @author Sandor Dornbush
*/ */
public class BluetoothConnectionManager { public class BluetoothConnectionManager {
// Unique UUID for this application
private static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB"); // Unique Bluetooth UUID for My Tracks
public static final UUID SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private MessageParser parser; private MessageParser parser;
@@ -254,41 +253,13 @@ public class BluetoothConnectionManager {
// Get a BluetoothSocket for a connection with the // Get a BluetoothSocket for a connection with the
// given BluetoothDevice // given BluetoothDevice
try { try {
tmp = getSocket(); tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) { } catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e); Log.e(Constants.TAG, "create() failed", e);
} }
socket = tmp; socket = tmp;
} }
private BluetoothSocket getSocket() throws IOException {
if (ApiFeatures.getInstance().hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord()) {
try {
return device.createInsecureRfcommSocketToServiceRecord(SPP_UUID);
} catch (IOException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
} else {
try {
Class<? extends BluetoothDevice> c = device.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(device, 1);
} catch (SecurityException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (NoSuchMethodException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalArgumentException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (IllegalAccessException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
} catch (InvocationTargetException e) {
Log.e(Constants.TAG, "Unable to get insecure connect.", e);
}
}
return device.createRfcommSocketToServiceRecord(SPP_UUID);
}
@Override @Override
public void run() { public void run() {
Log.d(Constants.TAG, "BEGIN mConnectThread"); Log.d(Constants.TAG, "BEGIN mConnectThread");
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services.sensors; package com.google.android.apps.mytracks.services.sensors;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import java.util.Arrays; import java.util.Arrays;
@@ -62,8 +62,7 @@ public class ZephyrMessageParser implements MessageParser {
// 0x1A00316550003162 produces erroneous values for Cadence and needs // 0x1A00316550003162 produces erroneous values for Cadence and needs
// a workaround based on the stride counter. // a workaround based on the stride counter.
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer. // Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
byte[] hardwareFirmwareId = ApiFeatures.getInstance().getApiAdapter() byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
.copyByteArray(buffer, 3, 11);
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder(); Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) { if (Arrays.equals(hardwareFirmwareId, CADENCE_BUG_FW_ID)) {
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException; import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.Context; import android.content.Context;
@@ -135,7 +135,7 @@ public class AntDirectSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE); Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit(); SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM); editor.putInt(context.getString(R.string.ant_heart_rate_sensor_id_key), deviceNumberHRM);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
private void handleMessageResponse(byte[] rawMessage) { private void handleMessageResponse(byte[] rawMessage) {
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException; import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.content.Sensor; import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.SystemUtils; import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
@@ -199,7 +199,7 @@ public class AntSrmBridgeSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE); Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit(); SharedPreferences.Editor editor = prefs.edit();
editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber); editor.putInt(context.getString(R.string.ant_srm_bridge_sensor_id_key), deviceNumber);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
private void handleMessageResponse(byte[] rawMessage) { private void handleMessageResponse(byte[] rawMessage) {
@@ -15,10 +15,9 @@
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.util.ApiFeatures; import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import android.content.Context; import android.content.Context;
import android.media.AudioManager;
/** /**
* Factory which wraps construction and setup of text-to-speech announcements in * Factory which wraps construction and setup of text-to-speech announcements in
@@ -28,30 +27,11 @@ import android.media.AudioManager;
*/ */
public class StatusAnnouncerFactory implements PeriodicTaskFactory { public class StatusAnnouncerFactory implements PeriodicTaskFactory {
private final boolean hasTts; public StatusAnnouncerFactory() {
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
} }
@Override @Override
public PeriodicTask create(Context context) { public PeriodicTask create(Context context) {
if (hasTts) { return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
return ApiFeatures.getInstance().getApiAdapter().getStatusAnnouncerTask(context);
} else {
return null;
}
}
/**
* Returns the appropriate volume stream for controlling announcement
* volume.
*/
public int getVolumeStream() {
if (hasTts) {
return StatusAnnouncerTask.getVolumeStream();
} else {
return AudioManager.USE_DEFAULT_STREAM_TYPE;
}
} }
} }
@@ -331,13 +331,6 @@ public class StatusAnnouncerTask implements PeriodicTask {
} }
} }
/**
* Returns the volume stream to use for controlling announcement volume.
*/
public static int getVolumeStream() {
return TextToSpeech.Engine.DEFAULT_STREAM;
}
/** /**
* Gets a string to announce the time. * Gets a string to announce the time.
* *
@@ -0,0 +1,44 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.util.Log;
import java.io.IOException;
/**
* API level 10 specific implementation of the {@link ApiAdapter}.
*
* @author Jimmy Shih
*/
public class Api10Adapter extends Api9Adapter {
@Override
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
try {
return bluetoothDevice.createInsecureRfcommSocketToServiceRecord(
BluetoothConnectionManager.SPP_UUID);
} catch (IOException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
}
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
};
}
@@ -15,50 +15,32 @@
*/ */
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import static com.google.android.apps.mytracks.Constants.TAG; import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.sensors.BluetoothConnectionManager;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask; import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask; import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.api.client.http.HttpTransport; import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.ApacheHttpTransport; import com.google.api.client.http.apache.ApacheHttpTransport;
import android.app.Notification; import android.bluetooth.BluetoothDevice;
import android.app.NotificationManager; import android.bluetooth.BluetoothSocket;
import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor; import android.content.SharedPreferences.Editor;
import android.util.Log; import android.util.Log;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException; import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method; import java.lang.reflect.Method;
/** /**
* API level 3 specific implementation of the {@link ApiLevelAdapter}. * API level 7 specific implementation of the {@link ApiAdapter}.
* *
* @author Bartlomiej Niechwiej * @author Bartlomiej Niechwiej
*/ */
public class ApiLevel3Adapter implements ApiLevelAdapter { public class Api7Adapter implements ApiAdapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
setServiceForeground(service, true);
notificationManager.notify(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
setServiceForeground(service, false);
if (id != -1) {
notificationManager.cancel(id);
}
}
@Override @Override
public PeriodicTask getStatusAnnouncerTask(Context context) { public PeriodicTask getStatusAnnouncerTask(Context context) {
return new StatusAnnouncerTask(context); return new StatusAnnouncerTask(context);
@@ -74,24 +56,6 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
}; };
} }
private void setServiceForeground(Service service, boolean foreground) {
// setForeground has been completely removed in API level 11, so we use reflection.
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(service, foreground);
} catch (SecurityException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to set service foreground state", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to set service foreground state", e);
}
}
@Override @Override
public void applyPreferenceChanges(Editor editor) { public void applyPreferenceChanges(Editor editor) {
editor.commit(); editor.commit();
@@ -114,4 +78,25 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
public HttpTransport getHttpTransport() { public HttpTransport getHttpTransport() {
return new ApacheHttpTransport(); return new ApacheHttpTransport();
} }
@Override
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException {
try {
Class<? extends BluetoothDevice> c = bluetoothDevice.getClass();
Method insecure = c.getMethod("createInsecureRfcommSocket", Integer.class);
insecure.setAccessible(true);
return (BluetoothSocket) insecure.invoke(bluetoothDevice, 1);
} catch (SecurityException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (NoSuchMethodException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (IllegalArgumentException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (IllegalAccessException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
} catch (InvocationTargetException e) {
Log.d(Constants.TAG, "Unable to create insecure connection", e);
}
return bluetoothDevice.createRfcommSocketToServiceRecord(BluetoothConnectionManager.SPP_UUID);
}
} }
@@ -1,3 +1,18 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener; import com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
@@ -8,11 +23,11 @@ import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import android.content.Context; import android.content.Context;
/** /**
* API level 8 specific implementation of the {@link ApiLevelAdapter}. * API level 8 specific implementation of the {@link ApiAdapter}.
* *
* @author Jimmy Shih * @author Jimmy Shih
*/ */
public class ApiLevel8Adapter extends ApiLevel5Adapter { public class Api8Adapter extends Api7Adapter {
@Override @Override
public PeriodicTask getStatusAnnouncerTask(Context context) { public PeriodicTask getStatusAnnouncerTask(Context context) {
@@ -1,3 +1,18 @@
/*
* Copyright 2012 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.Constants; import com.google.android.apps.mytracks.Constants;
@@ -11,11 +26,11 @@ import android.util.Log;
import java.util.Arrays; import java.util.Arrays;
/** /**
* API level 9 specific implementation of the {@link ApiLevelAdapter}. * API level 9 specific implementation of the {@link ApiAdapter}.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class ApiLevel9Adapter extends ApiLevel8Adapter { public class Api9Adapter extends Api8Adapter {
@Override @Override
public void applyPreferenceChanges(Editor editor) { public void applyPreferenceChanges(Editor editor) {
@@ -0,0 +1,101 @@
/*
* 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 com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.api.client.http.HttpTransport;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import java.io.IOException;
/**
* A set of methods that may be implemented differently depending on the Android
* API level.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiAdapter {
/**
* Gets a status announcer task.
* <p>
* Due to changes in API level 8.
*
* @param context the context
*/
public PeriodicTask getStatusAnnouncerTask(Context context);
/**
* Gets a {@link BackupPreferencesListener}.
* <p>
* Due to changes in API level 8.
*
* @param context the context
*/
public BackupPreferencesListener getBackupPreferencesListener(Context context);
/**
* Applies all the changes done to a given preferences editor. Changes may or
* may not be applied immediately.
* <p>
* Due to changes in API level 9.
*
* @param editor the editor
*/
public void applyPreferenceChanges(SharedPreferences.Editor editor);
/**
* Enables strict mode where supported, only if this is a development build.
* <p>
* Due to changes in API level 9.
*/
public void enableStrictMode();
/**
* Copies elements from an input byte array into a new byte array, from
* indexes start (inclusive) to end (exclusive). The end index must be less
* than or equal to the input length.
* <p>
* Due to changes in API level 9.
*
* @param input the input byte array
* @param start the start index
* @param end the end index
* @return a new array containing elements from the input byte array.
*/
public byte[] copyByteArray(byte[] input, int start, int end);
/**
* Gets a {@link HttpTransport}.
* <p>
* Due to changes in API level 9.
*/
public HttpTransport getHttpTransport();
/**
* Gets a {@link BluetoothSocket}.
* <p>
* Due to changes in API level 10.
*
* @param bluetoothDevice
*/
public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException;
}
@@ -0,0 +1,50 @@
/*
* 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.os.Build;
/**
* A factory to get the {@link ApiAdapter} for the current device.
*
* @author Rodrigo Damazio
*/
public class ApiAdapterFactory {
private static ApiAdapter apiAdapter;
/**
* Gets the {@link ApiAdapter} for the current device.
*/
public static ApiAdapter getApiAdapter() {
if (apiAdapter == null) {
if (Build.VERSION.SDK_INT >= 10) {
apiAdapter = new Api10Adapter();
return apiAdapter;
} else if (Build.VERSION.SDK_INT >= 9) {
apiAdapter = new Api9Adapter();
return apiAdapter;
} else if (Build.VERSION.SDK_INT >= 8) {
apiAdapter = new Api8Adapter();
return apiAdapter;
} else {
apiAdapter = new Api7Adapter();
return apiAdapter;
}
}
return apiAdapter;
}
}
@@ -1,122 +0,0 @@
/*
* 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 com.google.android.apps.mytracks.Constants;
import android.os.Build;
import android.util.Log;
/**
* Utility class for determining if newer-API features are available on the
* current device.
*
* @author Rodrigo Damazio
*/
public class ApiFeatures {
/**
* The API level of the Android version we are being run under.
*/
private static final int ANDROID_API_LEVEL = Integer.parseInt(
Build.VERSION.SDK);
private static ApiFeatures instance;
/**
* The API level adapter for the Android version we are being run under.
*/
private ApiLevelAdapter apiLevelAdapter;
/**
* Returns the singleton instance of this class.
*/
public static ApiFeatures getInstance() {
if (instance == null) {
instance = new ApiFeatures();
}
return instance;
}
/**
* Injects a specific singleton instance, to be used for unit tests.
*/
@SuppressWarnings("hiding")
public static void injectInstance(ApiFeatures instance) {
ApiFeatures.instance = instance;
}
/**
* Allow subclasses for mocking, but no direct instantiation.
*/
protected ApiFeatures() {
// It is safe to import unsupported classes as long as we only actually
// load the class when supported.
if (getApiLevel() >= 9) {
apiLevelAdapter = new ApiLevel9Adapter();
} else if (getApiLevel() >= 8) {
apiLevelAdapter = new ApiLevel8Adapter();
} else if (getApiLevel() >= 5) {
apiLevelAdapter = new ApiLevel5Adapter();
} else {
apiLevelAdapter = new ApiLevel3Adapter();
}
Log.i(Constants.TAG, "Using API level adapter " + apiLevelAdapter.getClass());
}
public ApiLevelAdapter getApiAdapter() {
return apiLevelAdapter;
}
// API Level 4 Changes
/**
* Returns whether text-to-speech is available.
*/
public boolean hasTextToSpeech() {
return getApiLevel() >= 4;
}
// API Level 5 Changes
/**
* There's a bug (#1587) in Cupcake and Donut which prevents you from
* using a SQLiteQueryBuilder twice. That is, if you call buildQuery
* on a given instance (to log the statement for debugging), and then
* call query on the same instance to make it actually do the query,
* it'll regenerate the query for the second call, and will screw it
* up. Specifically, it'll add extra parens which don't belong.
*/
public boolean canReuseSQLiteQueryBuilder() {
return getApiLevel() >= 5;
}
// API Level 10 changes
/**
* Returns true if BluetoothDevice.createInsecureRfcommSocketToServiceRecord
* is available.
*/
public boolean hasBluetoothDeviceCreateInsecureRfcommSocketToServiceRecord() {
return getApiLevel() >= 10;
}
// Visible for testing.
protected int getApiLevel() {
return ANDROID_API_LEVEL;
}
}
@@ -1,113 +0,0 @@
/*
* 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 com.google.android.apps.mytracks.io.backup.BackupPreferencesListener;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.api.client.http.HttpTransport;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.SharedPreferences;
/**
* A set of methods that may be implemented differently depending on the Android API level.
*
* @author Bartlomiej Niechwiej
*/
public interface ApiLevelAdapter {
/**
* Puts the specified service into foreground.
*
* Due to changes in API level 5.
*
* @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.
*
* Due to changes in API level 5.
*
* @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);
/**
* Gets a status announcer task.
*
* Due to changes in API level 8.
*/
PeriodicTask getStatusAnnouncerTask(Context context);
/**
* Gets a {@link BackupPreferencesListener}.
*
* Due to changes in API level 8.
*/
BackupPreferencesListener getBackupPreferencesListener(Context context);
/**
* Applies all changes done to the given preferences editor.
* Changes may or may not be applied immediately.
*
* Due to changes in API level 9.
*/
void applyPreferenceChanges(SharedPreferences.Editor editor);
/**
* Enables strict mode where supported, only if this is a development build.
*
* Due to changes in API level 9.
*/
void enableStrictMode();
/**
* Copies elements from the input byte array into a new byte array, from
* indexes start (inclusive) to end (exclusive). The end index must be less
* than or equal to input.length.
*
* Due to changes in API level 9.
*
* @param input the input byte array
* @param start the start index
* @param end the end index
* @return a new array containing elements from the input byte array
*/
byte[] copyByteArray(byte[] input, int start, int end);
/**
* Gets a {@link HttpTransport}.
*
* Due to changes in API level 9.
*/
HttpTransport getHttpTransport();
}
@@ -15,186 +15,49 @@
*/ */
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import com.google.android.apps.mytracks.Constants;
import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothDevice;
import android.os.Build;
import android.util.Log;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* Utilities for dealing with bluetooth devices. * Utilities for dealing with bluetooth devices.
* This can be used safely even in systems that don't support bluetooth,
* in which case a dummy implementation will be used.
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public abstract class BluetoothDeviceUtils { public class BluetoothDeviceUtils {
public static final String ANY_DEVICE = "any";
private static BluetoothDeviceUtils instance;
private BluetoothDeviceUtils() {}
/** /**
* Dummy implementation, for systems that don't support bluetooth. * Populates the device names and the device addresses with all the suitable
*/
private static class DummyImpl extends BluetoothDeviceUtils {
@Override
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
// Do nothing - no devices to add
}
@Override
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
return null;
}
}
/**
* Real implementation, for systems that DO support bluetooth.
*/
private static class RealImpl extends BluetoothDeviceUtils {
private final BluetoothAdapter bluetoothAdapter;
public RealImpl() {
bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (bluetoothAdapter == null) {
throw new IllegalStateException("Unable to get bluetooth adapter");
}
}
@Override
public void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses) {
ensureNotDiscovering();
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
BluetoothClass bluetoothClass = device.getBluetoothClass();
if (bluetoothClass != null) {
// Not really sure what we want, but I know what we don't want.
switch(bluetoothClass.getMajorDeviceClass()) {
case BluetoothClass.Device.Major.COMPUTER:
case BluetoothClass.Device.Major.PHONE:
break;
default:
deviceAddresses.add(device.getAddress());
deviceNames.add(device.getName());
}
}
}
}
@Override
public BluetoothDevice findDeviceMatching(String targetDeviceAddress) {
if (targetDeviceAddress.equals(ANY_DEVICE)) {
return findAnyDevice();
} else {
return findDeviceByAddress(targetDeviceAddress);
}
}
/**
* Finds and returns the first suitable bluetooth sensor.
*/
private BluetoothDevice findAnyDevice() {
ensureNotDiscovering();
Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
for (BluetoothDevice device : pairedDevices) {
// Look for the first paired computer device
if (isSuitableDevice(device)) {
return device;
}
}
return null;
}
/**
* Finds and returns a device with the given address, or null if it's not
* a suitable sensor.
*/
private BluetoothDevice findDeviceByAddress(String targetDeviceAddress) {
ensureNotDiscovering();
BluetoothDevice device = bluetoothAdapter.getRemoteDevice(targetDeviceAddress);
if (isSuitableDevice(device)) {
return device;
}
return null;
}
/**
* Ensures the bluetooth adapter is not in discovery mode.
*/
private void ensureNotDiscovering() {
// If it's in discovery mode, cancel that for now.
bluetoothAdapter.cancelDiscovery();
}
/**
* Checks whether the given device is a suitable sensor.
*
* @param device the device to check
* @return true if it's suitable, false otherwise
*/
private boolean isSuitableDevice(BluetoothDevice device) {
// Check that the device is bonded
if (device.getBondState() != BluetoothDevice.BOND_BONDED) {
return false;
}
return true;
}
}
/**
* Returns the proper (singleton) instance of this class.
*/
public static BluetoothDeviceUtils getInstance() {
if (instance == null) {
if (!isBluetoothMethodSupported()) {
Log.d(Constants.TAG, "Using dummy bluetooth utils");
instance = new DummyImpl();
} else {
Log.d(Constants.TAG, "Using real bluetooth utils");
try {
instance = new RealImpl();
} catch (IllegalStateException ise) {
Log.w(Constants.TAG, "Oops, I mean, using dummy bluetooth utils", ise);
instance = new DummyImpl();
}
}
}
return instance;
}
/**
* Populates the given lists with the names and addresses of all suitable
* bluetooth devices. * bluetooth devices.
* *
* @param deviceNames the list to populate with user-visible names * @param bluetoothAdapter the bluetooth adapter
* @param deviceAddresses the list to populate with device addresses * @param deviceNames list of device names
* @param deviceAddresses list of device addresses
*/ */
public abstract void populateDeviceLists(List<String> deviceNames, List<String> deviceAddresses); public static void populateDeviceLists(
BluetoothAdapter bluetoothAdapter, List<String> deviceNames, List<String> deviceAddresses) {
// Ensure the bluetooth adapter is not in discovery mode.
bluetoothAdapter.cancelDiscovery();
/** Set<BluetoothDevice> pairedDevices = bluetoothAdapter.getBondedDevices();
* Finds the bluetooth device with the given address. for (BluetoothDevice device : pairedDevices) {
* BluetoothClass bluetoothClass = device.getBluetoothClass();
* @param targetDeviceAddress the address of the device, or if (bluetoothClass != null) {
* {@link #ANY_DEVICE} for using the first suitable device // Not really sure what we want, but I know what we don't want.
* @return the device's descriptor, or null if not found switch (bluetoothClass.getMajorDeviceClass()) {
*/ case BluetoothClass.Device.Major.COMPUTER:
public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress); case BluetoothClass.Device.Major.PHONE:
break;
/** default:
* @return whether the bluetooth method is supported on this device deviceAddresses.add(device.getAddress());
*/ deviceNames.add(device.getName());
public static boolean isBluetoothMethodSupported() { }
return Integer.parseInt(Build.VERSION.SDK) >= 5; }
}
} }
} }
@@ -45,7 +45,7 @@ public class EulaUtil {
EULA_PREFERENCE_FILE, Context.MODE_PRIVATE); EULA_PREFERENCE_FILE, Context.MODE_PRIVATE);
Editor editor = preferences.edit(); Editor editor = preferences.edit();
editor.putBoolean(EULA_PREFERENCE_KEY, true); editor.putBoolean(EULA_PREFERENCE_KEY, true);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor); ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
} }
public static String getEulaMessage(Context context) { public static String getEulaMessage(Context context) {
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -154,7 +154,7 @@ public interface MyTracksProviderUtils {
/** /**
* Creates a cursor over the locations in the track points provider which * Creates a cursor over the locations in the track points provider which
* iterates over a given range of unique ids. * iterates over a given range of unique ids.
* Caller gets to own the returned cursor. Don't forget to close it. * Caller owns the returned cursor and is responsible for closing it.
* *
* @param trackId the id of the track for which to get the points * @param trackId the id of the track for which to get the points
* @param minTrackPointId the minimum id for the track points * @param minTrackPointId the minimum id for the track points
@@ -168,7 +168,7 @@ public interface MyTracksProviderUtils {
/** /**
* Creates a cursor over the waypoints of a track. * Creates a cursor over the waypoints of a track.
* Caller gets to own the returned cursor. Don't forget to close it. * Caller owns the returned cursor and is responsible for closing it.
* *
* @param trackId the id of the track for which to get the points * @param trackId the id of the track for which to get the points
* @param minWaypointId the minimum id for the track points * @param minWaypointId the minimum id for the track points
@@ -178,6 +178,18 @@ public interface MyTracksProviderUtils {
Cursor getWaypointsCursor(long trackId, long minWaypointId, Cursor getWaypointsCursor(long trackId, long minWaypointId,
int maxWaypoints); int maxWaypoints);
/**
* Creates a cursor over waypoints with the given selection.
* Caller owns the returned cursor and is responsible for closing it.
*
* @param selection a given selection
* @param selectionArgs arguments for the given selection
* @param order the order in which to return results
* @param maxWaypoints the maximum number of waypoints to return
* @return a cursor of the selected waypoints
*/
Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints);
/** /**
* Finds a track by given unique track id. * Finds a track by given unique track id.
* Note that the returned track object does not have any track points attached. * Note that the returned track object does not have any track points attached.
@@ -188,24 +200,26 @@ public interface MyTracksProviderUtils {
* @return a Track object, or null if not found * @return a Track object, or null if not found
*/ */
Track getTrack(long id); Track getTrack(long id);
/** /**
* Retrieves all tracks without track points. If no tracks exist, an empty * Retrieves all tracks without track points. If no tracks exist, an empty
* list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)} * list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)}
* to load the track points. * to load the track points.
* *
* @return a list of all the recorded tracks * @return a list of all the recorded tracks
*/ */
List<Track> getAllTracks(); List<Track> getAllTracks();
/** /**
* Creates a cursor over the tracks provider with a given selection. * Creates a cursor over the tracks provider with a given selection.
* Caller gets to own the returned cursor. Don't forget to close it. * Caller owns the returned cursor and is responsible for closing it.
* *
* @param selection a given selection * @param selection a given selection
* @param selecttionArgs parameters for the given selection
* @param order the order to return results in
* @return a cursor of the selected tracks * @return a cursor of the selected tracks
*/ */
Cursor getTracksCursor(String selection); Cursor getTracksCursor(String selection, String[] selectionArgs, String order);
/** /**
* Inserts a track in the tracks provider. * Inserts a track in the tracks provider.
@@ -288,7 +302,7 @@ public interface MyTracksProviderUtils {
/** /**
* Fill a location object with values from a given cursor. * Fill a location object with values from a given cursor.
* *
* @param cursor a cursor pointing at a db or provider with locations * @param cursor a cursor pointing at a db or provider with locations
* @param location a location object to be overwritten * @param location a location object to be overwritten
*/ */
@@ -308,7 +322,7 @@ public interface MyTracksProviderUtils {
interface LocationIterator extends Iterator<Location> { interface LocationIterator extends Iterator<Location> {
/** /**
* Returns ID of the most recently retrieved track point through a call to {@link #next()}. * Returns ID of the most recently retrieved track point through a call to {@link #next()}.
* *
* @return the ID of the most recent track point ID. * @return the ID of the most recent track point ID.
*/ */
long getLocationId(); long getLocationId();
@@ -328,12 +342,12 @@ public interface MyTracksProviderUtils {
* Creates a new {@link Location} object to be populated from the underlying database record. * Creates a new {@link Location} object to be populated from the underlying database record.
* It's up to the implementing class to decide whether to create a new instance or reuse * It's up to the implementing class to decide whether to create a new instance or reuse
* existing to optimize for speed. * existing to optimize for speed.
* *
* @return a {@link Location} to be populated from the database. * @return a {@link Location} to be populated from the database.
*/ */
Location createLocation(); Location createLocation();
} }
/** /**
* The default {@class Location}s factory, which creates a new location of 'gps' type. * The default {@class Location}s factory, which creates a new location of 'gps' type.
*/ */
@@ -361,20 +375,20 @@ public interface MyTracksProviderUtils {
return locs[lastLoc]; return locs[lastLoc];
} }
} }
/** /**
* Creates a new read-only iterator over all track points for the given track. It provides * Creates a new read-only iterator over all track points for the given track. It provides
* a lightweight way of iterating over long tracks without failing due to the underlying cursor * a lightweight way of iterating over long tracks without failing due to the underlying cursor
* limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws * limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws
* {@class UnsupportedOperationException}. * {@class UnsupportedOperationException}.
* *
* Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so, * Each call to {@link LocationIterator#next()} may advance to the next DB record, and if so,
* the iterator calls {@link LocationFactory#createLocation()} and populates it with information * the iterator calls {@link LocationFactory#createLocation()} and populates it with information
* retrieved from the record. * retrieved from the record.
* *
* When done with iteration, you must call {@link LocationIterator#close()} to make sure that all * When done with iteration, you must call {@link LocationIterator#close()} to make sure that all
* resources are properly deallocated. * resources are properly deallocated.
* *
* Example use: * Example use:
* <code> * <code>
* ... * ...
@@ -389,14 +403,14 @@ public interface MyTracksProviderUtils {
* } * }
* ... * ...
* </code> * </code>
* *
* @param trackId the ID of a track to retrieve locations for. * @param trackId the ID of a track to retrieve locations for.
* @param startTrackPointId the ID of the first track point to load, or -1 to start from * @param startTrackPointId the ID of the first track point to load, or -1 to start from
* the first point. * the first point.
* @param descending if true the results will be returned in descending ID * @param descending if true the results will be returned in descending ID
* order (latest location first). * order (latest location first).
* @param locationFactory the factory for creating new locations. * @param locationFactory the factory for creating new locations.
* *
* @return the read-only iterator over the given track's points. * @return the read-only iterator over the given track's points.
*/ */
LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending, LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending,
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.content; package com.google.android.apps.mytracks.content;
import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.TAG; import static com.google.android.apps.mytracks.lib.MyTracksLibConstants.TAG;
import com.google.android.apps.mytracks.content.Sensor;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
@@ -875,22 +875,34 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
} }
String selection; String selection;
String[] selectionArgs;
if (minWaypointId > 0) { if (minWaypointId > 0) {
selection = String.format("%s=%d AND %s>=%d", selection = String.format("%s = ? AND %s >= ?",
WaypointsColumns.TRACKID, trackId, WaypointsColumns.TRACKID,
WaypointsColumns._ID, minWaypointId); WaypointsColumns._ID);
selectionArgs = new String[] {
Long.toString(trackId),
Long.toString(minWaypointId)
};
} else { } else {
selection = String.format("%s=%d", selection = String.format("%s=?", WaypointsColumns.TRACKID);
WaypointsColumns.TRACKID, trackId); selectionArgs = new String[] { Long.toString(trackId) };
} }
String sortOrder = "_id ASC"; return getWaypointsCursor(selection, selectionArgs, null, maxWaypoints);
}
@Override
public Cursor getWaypointsCursor(String selection, String[] selectionArgs, String order, int maxWaypoints) {
if (order == null) {
order = "_id ASC";
}
if (maxWaypoints > 0) { if (maxWaypoints > 0) {
sortOrder += " LIMIT " + maxWaypoints; order += " LIMIT " + maxWaypoints;
} }
return contentResolver.query( return contentResolver.query(
WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder); WaypointsColumns.CONTENT_URI, null, selection, selectionArgs, order);
} }
@Override @Override
@@ -905,7 +917,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override @Override
public List<Track> getAllTracks() { public List<Track> getAllTracks() {
Cursor cursor = getTracksCursor(null); Cursor cursor = getTracksCursor(null, null, TracksColumns._ID);
ArrayList<Track> tracks = new ArrayList<Track>(); ArrayList<Track> tracks = new ArrayList<Track>();
if (cursor != null) { if (cursor != null) {
tracks.ensureCapacity(cursor.getCount()); tracks.ensureCapacity(cursor.getCount());
@@ -923,10 +935,9 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
} }
@Override @Override
public Cursor getTracksCursor(String selection) { public Cursor getTracksCursor(String selection, String[] selectionArgs, String order) {
Cursor cursor = contentResolver.query( return contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, null, "_id"); TracksColumns.CONTENT_URI, null, selection, selectionArgs, order);
return cursor;
} }
@Override @Override
@@ -1,12 +1,12 @@
/* /*
* Copyright 2010 Google Inc. * Copyright 2010 Google Inc.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); you may not * 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 * use this file except in compliance with the License. You may obtain a copy of
* the License at * the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
@@ -26,7 +26,7 @@ import android.os.Parcelable;
* *
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class TripStatistics implements Parcelable { public class TripStatistics implements Parcelable {
/** /**
* The start time for the trip. This is system time which might not match gps * The start time for the trip. This is system time which might not match gps
@@ -144,7 +144,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the time that this track started. * Gets the time that this track started.
* *
* @return The number of milliseconds since epoch to the time when this track * @return The number of milliseconds since epoch to the time when this track
* started * started
*/ */
@@ -154,7 +154,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the time that this track stopped. * Gets the time that this track stopped.
* *
* @return The number of milliseconds since epoch to the time when this track * @return The number of milliseconds since epoch to the time when this track
* stopped * stopped
*/ */
@@ -176,7 +176,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the total distance the user traveled. * Gets the total distance the user traveled.
* *
* @return The total distance traveled in meters * @return The total distance traveled in meters
*/ */
public double getTotalDistance() { public double getTotalDistance() {
@@ -197,7 +197,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the the average speed the user traveled when they were actively * Gets the the average speed the user traveled when they were actively
* moving. * moving.
* *
* @return The average moving speed in m/s * @return The average moving speed in m/s
*/ */
public double getAverageMovingSpeed() { public double getAverageMovingSpeed() {
@@ -206,7 +206,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the the maximum speed for this track. * Gets the the maximum speed for this track.
* *
* @return The maximum speed in m/s * @return The maximum speed in m/s
*/ */
public double getMaxSpeed() { public double getMaxSpeed() {
@@ -215,7 +215,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the moving time. * Gets the moving time.
* *
* @return The total number of milliseconds the user was moving * @return The total number of milliseconds the user was moving
*/ */
public long getMovingTime() { public long getMovingTime() {
@@ -225,7 +225,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the total elevation gain for this trip. This is calculated as the sum * Gets the total elevation gain for this trip. This is calculated as the sum
* of all positive differences in the smoothed elevation. * of all positive differences in the smoothed elevation.
* *
* @return The elevation gain in meters for this trip * @return The elevation gain in meters for this trip
*/ */
public double getTotalElevationGain() { public double getTotalElevationGain() {
@@ -233,39 +233,79 @@ public class TripStatistics implements Parcelable {
} }
/** /**
* Returns the leftmost position (lowest longitude) of the track, in signed * Returns the leftmost position (lowest longitude) of the track, in signed degrees.
* decimal degrees. */
public double getLeftDegrees() {
return longitudeExtremities.getMin();
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees.
*/ */
public int getLeft() { public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6); return (int) (longitudeExtremities.getMin() * 1E6);
} }
/** /**
* Returns the rightmost position (highest longitude) of the track, in signed * Returns the rightmost position (highest longitude) of the track, in signed degrees.
* decimal degrees. */
public double getRightDegrees() {
return longitudeExtremities.getMax();
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed millions of degrees.
*/ */
public int getRight() { public int getRight() {
return (int) (longitudeExtremities.getMax() * 1E6); return (int) (longitudeExtremities.getMax() * 1E6);
} }
/** /**
* Returns the bottommost position (lowest latitude) of the track, in meters. * Returns the bottommost position (lowest latitude) of the track, in signed degrees.
*/
public double getBottomDegrees() {
return latitudeExtremities.getMin();
}
/**
* Returns the bottommost position (lowest latitude) of the track, in signed millions of degrees.
*/ */
public int getBottom() { public int getBottom() {
return (int) (latitudeExtremities.getMin() * 1E6); return (int) (latitudeExtremities.getMin() * 1E6);
} }
/** /**
* Returns the topmost position (highest latitude) of the track, in meters. * Returns the topmost position (highest latitude) of the track, in signed degrees.
*/
public double getTopDegrees() {
return latitudeExtremities.getMax();
}
/**
* Returns the topmost position (highest latitude) of the track, in signed millions of degrees.
*/ */
public int getTop() { public int getTop() {
return (int) (latitudeExtremities.getMax() * 1E6); return (int) (latitudeExtremities.getMax() * 1E6);
} }
/**
* Returns the mean position (center latitude) of the track, in signed degrees.
*/
public double getMeanLatitude() {
return (getBottomDegrees() + getTopDegrees()) / 2.0;
}
/**
* Returns the mean position (center longitude) of the track, in signed degrees.
*/
public double getMeanLongitude() {
return (getLeftDegrees() + getRightDegrees()) / 2.0;
}
/** /**
* Gets the minimum elevation seen on this trip. This is calculated from the * Gets the minimum elevation seen on this trip. This is calculated from the
* smoothed elevation so this can actually be more than the current elevation. * smoothed elevation so this can actually be more than the current elevation.
* *
* @return The smallest elevation reading for this trip in meters * @return The smallest elevation reading for this trip in meters
*/ */
public double getMinElevation() { public double getMinElevation() {
@@ -275,7 +315,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the maximum elevation seen on this trip. This is calculated from the * Gets the maximum elevation seen on this trip. This is calculated from the
* smoothed elevation so this can actually be less than the current elevation. * smoothed elevation so this can actually be less than the current elevation.
* *
* @return The largest elevation reading for this trip in meters * @return The largest elevation reading for this trip in meters
*/ */
public double getMaxElevation() { public double getMaxElevation() {
@@ -284,7 +324,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the maximum grade for this trip. * Gets the maximum grade for this trip.
* *
* @return The maximum grade for this trip as a fraction * @return The maximum grade for this trip as a fraction
*/ */
public double getMaxGrade() { public double getMaxGrade() {
@@ -293,7 +333,7 @@ public class TripStatistics implements Parcelable {
/** /**
* Gets the minimum grade for this trip. * Gets the minimum grade for this trip.
* *
* @return The minimum grade for this trip as a fraction * @return The minimum grade for this trip as a fraction
*/ */
public double getMinGrade() { public double getMinGrade() {
@@ -443,7 +483,7 @@ public class TripStatistics implements Parcelable {
void addMovingTime(long time) { void addMovingTime(long time) {
movingTime += time; movingTime += time;
} }
/** /**
* Accounts for a new latitude value for the bounding box. * Accounts for a new latitude value for the bounding box.
* *
@@ -0,0 +1,313 @@
/**
*
*/
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.content.SearchEngine.ScoredResult;
import com.google.android.apps.mytracks.content.SearchEngine.SearchQuery;
import com.google.android.apps.mytracks.services.TrackRecordingServiceTest.MockContext;
import com.google.android.apps.mytracks.stats.TripStatistics;
import android.content.ContentUris;
import android.location.Location;
import android.net.Uri;
import android.test.AndroidTestCase;
import android.test.RenamingDelegatingContext;
import android.test.mock.MockContentResolver;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Tests for {@link SearchEngine}.
* These are not meant to be quality tests, but instead feature-by-feature tests
* (in other words, they don't test the mixing of different score boostings, just
* each boosting separately)
*
* @author Rodrigo Damazio
*/
public class SearchEngineTest extends AndroidTestCase {
private static final Location HERE = new Location("gps");
private static final long NOW = 1234567890000L; // After OLDEST_ALLOWED_TIMESTAMP
private MyTracksProviderUtils providerUtils;
private SearchEngine engine;
@Override
protected void setUp() throws Exception {
super.setUp();
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
MockContext 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);
engine = new SearchEngine(providerUtils);
}
@Override
protected void tearDown() throws Exception {
providerUtils.deleteAllTracks();
super.tearDown();
}
private long insertTrack(String title, String description, String category, double distance, long hoursAgo) {
Track track = new Track();
track.setName(title);
track.setDescription(description);
track.setCategory(category);
TripStatistics stats = track.getStatistics();
if (hoursAgo > 0) {
// Started twice hoursAgo, so the average time is hoursAgo.
stats.setStartTime(NOW - hoursAgo * 1000L * 60L * 60L * 2);
stats.setStopTime(NOW);
}
int latitude = (int) ((HERE.getLatitude() + distance) * 1E6);
int longitude = (int) ((HERE.getLongitude() + distance) * 1E6);
stats.setBounds(latitude, longitude, latitude, longitude);
Uri uri = providerUtils.insertTrack(track);
return ContentUris.parseId(uri);
}
private long insertTrack(String title, String description, String category) {
return insertTrack(title, description, category, 0, -1);
}
private long insertTrack(String title, double distance) {
return insertTrack(title, "", "", distance, -1);
}
private long insertTrack(String title, long hoursAgo) {
return insertTrack(title, "", "", 0.0, hoursAgo);
}
private long insertWaypoint(String title, String description, String category, double distance, long hoursAgo, long trackId) {
Waypoint waypoint = new Waypoint();
waypoint.setName(title);
waypoint.setDescription(description);
waypoint.setCategory(category);
waypoint.setTrackId(trackId);
Location location = new Location(HERE);
location.setLatitude(location.getLatitude() + distance);
location.setLongitude(location.getLongitude() + distance);
if (hoursAgo >= 0) {
location.setTime(NOW - hoursAgo * 1000L * 60L * 60L);
}
waypoint.setLocation(location);
Uri uri = providerUtils.insertWaypoint(waypoint);
return ContentUris.parseId(uri);
}
private long insertWaypoint(String title, String description, String category) {
return insertWaypoint(title, description, category, 0.0, -1, -1);
}
private long insertWaypoint(String title, double distance) {
return insertWaypoint(title, "", "", distance, -1, -1);
}
private long insertWaypoint(String title, long hoursAgo) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, -1);
}
private long insertWaypoint(String title, long hoursAgo, long trackId) {
return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId);
}
public void testSearchText() {
// Insert 7 tracks (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertTrack("bb", "cc", "dd");
long descriptionMatchId = insertTrack("bb", "aa", "cc");
long categoryMatchId = insertTrack("bb", "cc", "aa");
long titleMatchId = insertTrack("aa", "bb", "cc");
long titleCategoryMatchId = insertTrack("aa", "bb", "ca");
long titleDescriptionMatchId = insertTrack("aa", "ba", "cc");
long allMatchId = insertTrack("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertTrackResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchWaypointText() {
// Insert 7 waypoints (purposefully out of result order):
// - one which won't match
// - one which will match the description
// - one which will match the category
// - one which will match the title
// - one which will match in title and category
// - one which will match in title and description
// - one which will match in all fields
insertWaypoint("bb", "cc", "dd");
long descriptionMatchId = insertWaypoint("bb", "aa", "cc");
long categoryMatchId = insertWaypoint("bb", "cc", "aa");
long titleMatchId = insertWaypoint("aa", "bb", "cc");
long titleCategoryMatchId = insertWaypoint("aa", "bb", "ca");
long titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc");
long allMatchId = insertWaypoint("aa", "ba", "ca");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertWaypointResults(results,
allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId,
categoryMatchId);
}
public void testSearchMixedText() {
// Insert 5 entries (purposefully out of result order):
// - one waypoint which will match by description
// - one waypoint which won't match
// - one waypoint which will match by title
// - one track which won't match
// - one track which will match by title
long descriptionWaypointId = insertWaypoint("bb", "aa", "cc");
insertWaypoint("bb", "cc", "dd");
long titleWaypointId = insertWaypoint("aa", "bb", "cc");
insertTrack("bb", "cc", "dd");
long trackId = insertTrack("aa", "bb", "cc");
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Title > Description > Category.
assertEquals(results.toString(), 3, results.size());
assertTrackResult(trackId, results.get(0));
assertWaypointResult(titleWaypointId, results.get(1));
assertWaypointResult(descriptionWaypointId, results.get(2));
}
public void testSearchTrackDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertTrack("aa", 0.3);
long nearId = insertTrack("ab", 0.1);
long farId = insertTrack("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertTrackResults(results, nearId, farId, farFarAwayId);
}
public void testSearchWaypointDistance() {
// All results match text, but they're at difference distances from the user.
long farFarAwayId = insertWaypoint("aa", 0.3);
long nearId = insertWaypoint("ab", 0.1);
long farId = insertWaypoint("ac", 0.2);
SearchQuery query = new SearchQuery("a", HERE, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Distance order.
assertWaypointResults(results, nearId, farId, farFarAwayId);
}
public void testSearchTrackRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertTrack("aa", 3);
long recentId = insertTrack("ab", 1);
long oldId = insertTrack("ac", 2);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertTrackResults(results, recentId, oldId, oldestId);
}
public void testSearchWaypointRecent() {
// All results match text, but they're were recorded at different times.
long oldestId = insertWaypoint("aa", 2);
long recentId = insertWaypoint("ab", 0);
long oldId = insertWaypoint("ac", 1);
SearchQuery query = new SearchQuery("a", null, -1, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Reverse time order.
assertWaypointResults(results, recentId, oldId, oldestId);
}
public void testSearchCurrentTrack() {
// All results match text, but one of them is the current track.
long currentId = insertTrack("ab", 1);
long otherId = insertTrack("aa", 1);
SearchQuery query = new SearchQuery("a", null, currentId, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Current track should be demoted.
assertTrackResults(results, otherId, currentId);
}
public void testSearchCurrentTrackWaypoint() {
// All results match text, but one of them is in the current track.
long otherId = insertWaypoint("aa", 1, 456);
long currentId = insertWaypoint("ab", 1, 123);
SearchQuery query = new SearchQuery("a", null, 123, NOW);
ArrayList<ScoredResult> results = new ArrayList<ScoredResult>(engine.search(query));
// Waypoint in current track should be promoted.
assertWaypointResults(results, currentId, otherId);
}
private void assertTrackResult(long trackId, ScoredResult result) {
assertNotNull("Not a track", result.track);
assertNull("Ambiguous result", result.waypoint);
assertEquals(trackId, result.track.getId());
}
private void assertTrackResults(List<ScoredResult> results, long... trackIds) {
String errMsg = "Expected IDs=" + Arrays.toString(trackIds) + "; results=" + results;
assertEquals(results.size(), trackIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.track);
assertNull(errMsg, result.waypoint);
assertEquals(errMsg, trackIds[i], result.track.getId());
}
}
private void assertWaypointResult(long waypointId, ScoredResult result) {
assertNotNull("Not a waypoint", result.waypoint);
assertNull("Ambiguous result", result.track);
assertEquals(waypointId, result.waypoint.getId());
}
private void assertWaypointResults(List<ScoredResult> results, long... waypointIds) {
String errMsg = "Expected IDs=" + Arrays.toString(waypointIds) + "; results=" + results;
assertEquals(results.size(), waypointIds.length);
for (int i = 0; i < results.size(); i++) {
ScoredResult result = results.get(i);
assertNotNull(errMsg, result.waypoint);
assertNull(errMsg, result.track);
assertEquals(errMsg, waypointIds[i], result.waypoint.getId());
}
}
}
@@ -0,0 +1,64 @@
// Copyright 2012 Google Inc. All Rights Reserved.
package com.google.android.apps.mytracks.services;
import android.app.Notification;
import android.app.Service;
import android.test.ServiceTestCase;
import android.util.Log;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* A {@link TrackRecordingService} that can be used with {@link ServiceTestCase}.
* {@link ServiceTestCase} throws a null pointer exception when the service
* calls {@link Service#startForeground(int, android.app.Notification)} and
* {@link Service#stopForeground(boolean)}.
* <p>
* See http://code.google.com/p/android/issues/detail?id=12122
* <p>
* Wrap these two methods in wrappers and override them.
*
* @author Jimmy Shih
*/
public class TestRecordingService extends TrackRecordingService {
private static final String TAG = TestRecordingService.class.getSimpleName();
@Override
protected void startForegroundService(Notification notification) {
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(this, true);
} catch (SecurityException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
}
}
@Override
protected void stopForegroundService() {
try {
Method setForegroundMethod = Service.class.getMethod("setForeground", boolean.class);
setForegroundMethod.invoke(this, false);
} catch (SecurityException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (NoSuchMethodException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (IllegalAccessException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
} catch (InvocationTargetException e) {
Log.e(TAG, "Unable to start a service in foreground", e);
}
}
}
@@ -24,7 +24,6 @@ import com.google.android.apps.mytracks.content.Track;
import com.google.android.apps.mytracks.content.Waypoint; import com.google.android.apps.mytracks.content.Waypoint;
import com.google.android.apps.mytracks.content.WaypointCreationRequest; import com.google.android.apps.mytracks.content.WaypointCreationRequest;
import com.google.android.apps.mytracks.stats.TripStatistics; import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R; import com.google.android.maps.mytracks.R;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
@@ -57,8 +56,7 @@ import java.util.List;
* is called, which prevents from testing multiple scenarios in a single * is called, which prevents from testing multiple scenarios in a single
* test (see runFunctionTest for more details). * test (see runFunctionTest for more details).
*/ */
public class TrackRecordingServiceTest public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingService> {
extends ServiceTestCase<TrackRecordingService> {
private Context context; private Context context;
private MyTracksProviderUtils providerUtils; private MyTracksProviderUtils providerUtils;
@@ -74,7 +72,7 @@ public class TrackRecordingServiceTest
private Intent serviceIntent; private Intent serviceIntent;
public TrackRecordingServiceTest() { public TrackRecordingServiceTest() {
super(TrackRecordingService.class); super(TestRecordingService.class);
} }
/** /**
@@ -96,19 +94,6 @@ public class TrackRecordingServiceTest
} }
} }
/**
* 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 @Override
protected IBinder bindService(Intent intent) { protected IBinder bindService(Intent intent) {
if (getService() != null) { if (getService() != null) {
@@ -138,8 +123,6 @@ public class TrackRecordingServiceTest
protected void setUp() throws Exception { protected void setUp() throws Exception {
super.setUp(); super.setUp();
ApiFeatures.injectInstance(new MockApiFeatures());
MockContentResolver mockContentResolver = new MockContentResolver(); MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext( RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test."); getContext(), getContext(), "test.");
@@ -15,13 +15,6 @@
*/ */
package com.google.android.apps.mytracks.services.tasks; package com.google.android.apps.mytracks.services.tasks;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiFeatures;
import android.media.AudioManager;
import android.speech.tts.TextToSpeech;
import android.test.AndroidTestCase; import android.test.AndroidTestCase;
/** /**
@@ -31,62 +24,9 @@ import android.test.AndroidTestCase;
* @author Rodrigo Damazio * @author Rodrigo Damazio
*/ */
public class StatusAnnouncerFactoryTest extends AndroidTestCase { public class StatusAnnouncerFactoryTest extends AndroidTestCase {
/**
* Mock version of the {@link ApiFeatures} class.
*/
private class MockApiFeatures extends ApiFeatures {
private boolean hasTts;
public void setHasTextToSpeech(boolean hasTts) {
this.hasTts = hasTts;
}
@Override
public boolean hasTextToSpeech() {
return hasTts;
}
}
private MockApiFeatures apiFeatures;
@Override
protected void setUp() throws Exception {
super.setUp();
apiFeatures = new MockApiFeatures();
}
public void testCreate() { public void testCreate() {
apiFeatures.setHasTextToSpeech(true); PeriodicTaskFactory factory = new StatusAnnouncerFactory();
PeriodicTaskFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTask task = factory.create(getContext()); PeriodicTask task = factory.create(getContext());
assertTrue(task instanceof StatusAnnouncerTask); assertTrue(task instanceof StatusAnnouncerTask);
} }
public void testCreate_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
PeriodicTaskFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTask task = factory.create(getContext());
assertNull(task);
}
public void testGetVolumeStream() {
apiFeatures.setHasTextToSpeech(true);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
TextToSpeech.Engine.DEFAULT_STREAM,
factory.getVolumeStream());
}
public void testGetVolumeStream_notAvailable() {
apiFeatures.setHasTextToSpeech(false);
StatusAnnouncerFactory factory =
new StatusAnnouncerFactory(apiFeatures);
assertEquals(
AudioManager.USE_DEFAULT_STREAM_TYPE,
factory.getVolumeStream());
}
} }
@@ -15,27 +15,15 @@
*/ */
package com.google.android.apps.mytracks.util; package com.google.android.apps.mytracks.util;
import android.app.Notification; import junit.framework.TestCase;
import android.app.NotificationManager;
import android.app.Service;
/** /**
* API level 5 specific implementation of the {@link ApiLevelAdapter}. * Tests {@link ApiAdapterFactory}.
* *
* @author Bartlomiej Niechwiej * @author Rodrigo Damazio
*/ */
public class ApiLevel5Adapter extends ApiLevel3Adapter { public class ApiAdapterFactoryTest extends TestCase {
public void testGetApiLevelAdapter() {
@Override assertNotNull(ApiAdapterFactory.getApiAdapter());
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);
} }
} }
@@ -1,62 +0,0 @@
/*
* 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 junit.framework.TestCase;
/**
* Tests the API feature detection code in {@link ApiFeatures}.
* This test requires Froyo+ to run.
*
* @author Rodrigo Damazio
*/
public class ApiFeaturesTest extends TestCase {
private TestableApiFeatures features;
private class TestableApiFeatures extends ApiFeatures {
private int apiLevel;
public void setApiLevel(int apiLevel) {
this.apiLevel = apiLevel;
}
@Override
protected int getApiLevel() {
return apiLevel;
}
}
@Override
protected void setUp() throws Exception {
super.setUp();
features = new TestableApiFeatures();
}
public void testHasTextToSpeech() {
features.setApiLevel(3);
assertFalse(features.hasTextToSpeech());
for (int i = 4; i <= 8; i++) {
features.setApiLevel(i);
assertTrue(features.hasTextToSpeech());
}
}
public void testGetApiAdapter() {
assertNotNull(features.getApiAdapter());
}
}