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"
android:value="AEdPqrEAAAAIi-_QiwoRSc9_bAC9cmuNXTQyU8ajJmGtKdhskQ" />
<!-- Main track data provider -->
<provider android:name="com.google.android.apps.mytracks.content.MyTracksProvider"
android:authorities="com.google.android.maps.mytracks"
android:exported="true"
android:readPermission="com.google.android.apps.mytracks.READ_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"
android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|fontScale">
<intent-filter>
@@ -129,7 +136,12 @@ limitations under the License.
<category android:name="android.intent.category.DEFAULT" />
<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.waypoint" />
</intent-filter>
<!-- 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.MapActivity"
@@ -137,13 +149,26 @@ limitations under the License.
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
<!-- 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 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 -->
<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"
android:label="@string/track_detail_title">
@@ -168,7 +193,6 @@ limitations under the License.
<activity android:name="com.google.android.apps.mytracks.WaypointDetails"
android:label="@string/marker_detail_title">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.EDIT" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
@@ -191,6 +215,10 @@ limitations under the License.
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
<!-- 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.SensorStateActivity"
@@ -201,6 +229,10 @@ limitations under the License.
<intent-filter>
<category android:name="android.intent.category.TAB" />
</intent-filter>
<!-- 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.io.sendtogoogle.SendActivity"
@@ -241,6 +273,15 @@ limitations under the License.
android:label="@string/welcome_title"
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.fusiontables.SendFusionTablesActivity"/>
<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"
android:exported="false" />
<receiver android:name="com.google.android.apps.mytracks.BootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
+22 -3
View File
@@ -1,4 +1,19 @@
<?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">
<item android:id="@+id/menu_record_track"
android:icon="@drawable/ic_menu_recordtrack"
@@ -16,20 +31,24 @@
android:icon="@drawable/ic_menu_waypoints"
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"
android:icon="@drawable/ic_menu_sensorstatus"
android:orderInCategory="1000"
android:title="@string/menu_sensor_state"/>
<item android:id="@+id/menu_settings"
android:icon="@android:drawable/ic_menu_preferences"
android:orderInCategory="1001"
android:title="@string/menu_settings"/>
<item android:id="@+id/menu_aggregated_statistics"
android:orderInCategory="1002"
android:title="@string/menu_aggregated_statistics"/>
<item android:id="@+id/menu_help"
android:icon="@android:drawable/ic_menu_info_details"
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">
Tracks
</string>
<string name="menu_search"
translation_description="In the menu, to start a textual search for tracks or waypoints.">
Search
</string>
<!-- My Location -->
<string name="my_location_approximate_location"
@@ -1092,6 +1096,24 @@ limitations under the License.
Finished writing to the SD card.
</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 -->
<string name="send_google_by_my_tracks"
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">
Voice
</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"
translation_description="In the 'Recording' settings, the summary for the '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.TrackDataListener;
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.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
@@ -40,6 +39,7 @@ import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -116,9 +116,7 @@ public class ChartActivity extends Activity implements TrackDataListener {
super.onCreate(savedInstanceState);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.mytracks_charts);
@@ -16,7 +16,7 @@
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 android.app.AlertDialog;
@@ -63,8 +63,7 @@ class CheckUnits {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = useMetricPreferences.edit();
String key = context.getString(R.string.metric_units_key);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
editor.putBoolean(key, metric));
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor.putBoolean(key, metric));
}
});
builder.setOnCancelListener(new DialogInterface.OnCancelListener() {
@@ -76,7 +75,7 @@ class CheckUnits {
}
private static void recordCheckPerformed(SharedPreferences preferences) {
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putBoolean(CHECK_UNITS_PREFERENCE_KEY, true));
}
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks;
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 android.app.AlertDialog;
@@ -66,7 +66,7 @@ public class DeleteAllTracks extends Handler {
Editor editor = prefs.edit();
// TODO: Go through data manager
editor.putLong(context.getString(R.string.selected_track_key), -1);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
if (done != null) {
Handler h = new Handler();
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.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.maps.mytracks.R;
@@ -110,7 +110,7 @@ public class DeleteTrack extends Activity
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
if (preferences.getLong(selectedKey, -1) == deleteTrackId) {
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 fileTypes[] = activity.getResources().getStringArray(R.array.file_types);
String[] choices = new String[fileTypes.length];
for (int i = 0; i < fileTypes.length; i++) {
choices[i] = String.format(exportFileFormat, fileTypes[i]);
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
builder.setTitle(R.string.track_list_export_all);
builder.setSingleChoiceItems(choices, 0, itemClick);
@@ -160,7 +160,7 @@ public class ExportAllTracks {
try {
MyTracksProviderUtils providerUtils =
MyTracksProviderUtils.Factory.get(activity);
cursor = providerUtils.getTracksCursor("");
cursor = providerUtils.getTracksCursor(null, null, TracksColumns._ID);
if (cursor == null) {
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.sendtogoogle.SendType;
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.util.ApiFeatures;
import com.google.android.apps.mytracks.util.GeoRect;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.maps.GeoPoint;
@@ -44,6 +43,7 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -86,6 +86,25 @@ public class MapActivity extends com.google.android.maps.MapActivity
*/
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.
* 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);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
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
public void onSelectedTrackChanged(final Track track, final boolean isRecording) {
runOnUiThread(new Runnable() {
@@ -345,7 +403,12 @@ public class MapActivity extends com.google.android.maps.MapActivity
if (trackSelected) {
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);
busyPane.setVisibility(View.GONE);
@@ -37,6 +37,8 @@ class MenuManager {
public boolean onCreateOptionsMenu(Menu menu) {
activity.getMenuInflater().inflate(R.menu.main, menu);
// TODO: Replace search button with search widget if API level >= 11
return true;
}
@@ -85,6 +87,10 @@ class MenuManager {
case R.id.menu_help: {
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;
}
@@ -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.TrackDataHub;
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.WaypointsColumns;
import com.google.android.apps.mytracks.io.file.TempFileCleaner;
import com.google.android.apps.mytracks.services.ITrackRecordingService;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.TrackRecordingServiceConnection;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.EulaUtil;
import com.google.android.apps.mytracks.util.SystemUtils;
import com.google.android.apps.mytracks.util.UriUtils;
@@ -46,6 +48,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.os.RemoteException;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
@@ -67,7 +70,7 @@ import android.widget.Toast;
@SuppressWarnings("deprecation")
public class MyTracks extends TabActivity implements OnTouchListener {
private static final int DIALOG_EULA_ID = 0;
private TrackDataHub dataHub;
/**
@@ -139,9 +142,8 @@ public class MyTracks extends TabActivity implements OnTouchListener {
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "MyTracks.onCreate");
super.onCreate(savedInstanceState);
ApiFeatures apiFeatures = ApiFeatures.getInstance();
if (!SystemUtils.isRelease(this)) {
apiFeatures.getApiAdapter().enableStrictMode();
ApiAdapterFactory.getApiAdapter().enableStrictMode();
}
tracker = GoogleAnalyticsTracker.getInstance();
@@ -157,14 +159,14 @@ public class MyTracks extends TabActivity implements OnTouchListener {
menuManager = new MenuManager(this);
serviceConnection = new TrackRecordingServiceConnection(this, serviceBindCallback);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
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 TabHost tabHost = getTabHost();
tabHost.addTab(tabHost.newTabSpec("tab1")
@@ -199,7 +201,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
showDialog(DIALOG_EULA_ID);
}
}
@Override
protected void onStart() {
Log.d(TAG, "MyTracks.onStart");
@@ -219,6 +221,19 @@ public class MyTracks extends TabActivity implements OnTouchListener {
&& UriUtils.matchesContentUri(data, TracksColumns.CONTENT_URI)) {
long trackId = ContentUris.parseId(data);
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;
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
@@ -366,12 +381,7 @@ public class MyTracks extends TabActivity implements OnTouchListener {
if (results != null) {
final long waypointId = results.getLongExtra(WaypointDetails.WAYPOINT_ID_EXTRA, -1);
if (waypointId >= 0) {
MapActivity map =
(MapActivity) getLocalActivityManager().getActivity("tab1");
if (map != null) {
getTabHost().setCurrentTab(0);
map.showWaypoint(waypointId);
}
showWaypoint(trackId, waypointId);
}
}
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
public boolean onTouch(View v, MotionEvent event) {
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.BackupPreferencesListener;
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.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiAdapterFactory;
import com.google.android.apps.mytracks.util.BluetoothDeviceUtils;
import com.google.android.apps.mytracks.util.UnitConversions;
import com.google.android.maps.mytracks.R;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
@@ -44,6 +44,7 @@ import android.preference.PreferenceCategory;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.provider.Settings;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import android.widget.Toast;
@@ -102,10 +103,7 @@ public class SettingsActivity extends PreferenceActivity {
super.onCreate(icicle);
// The volume we want to control is the Text-To-Speech volume
ApiFeatures apiFeatures = ApiFeatures.getInstance();
int volumeStream =
new StatusAnnouncerFactory(apiFeatures).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// Tell it where to read/write preferences
PreferenceManager preferenceManager = getPreferenceManager();
@@ -113,23 +111,13 @@ public class SettingsActivity extends PreferenceActivity {
preferenceManager.setSharedPreferencesMode(0);
// Set up automatic preferences backup
backupListener = apiFeatures.getApiAdapter().getBackupPreferencesListener(this);
backupListener = ApiAdapterFactory.getApiAdapter().getBackupPreferencesListener(this);
preferences = preferenceManager.getSharedPreferences();
preferences.registerOnSharedPreferenceChangeListener(backupListener);
// Load the preferences to be displayed
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();
setAutoResumeTimeoutOptions();
@@ -557,20 +545,17 @@ public class SettingsActivity extends PreferenceActivity {
* Configures preference actions related to bluetooth.
*/
private void configureBluetoothPreferences() {
if (BluetoothDeviceUtils.isBluetoothMethodSupported()) {
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key))
.setOnPreferenceClickListener(new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
// Populate the list of bluetooth devices
populateBluetoothDeviceList();
// Make the pair devices preference go to the system preferences
findPreference(getString(R.string.bluetooth_pairing_key)).setOnPreferenceClickListener(
new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
Intent settingsIntent = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS);
startActivity(settingsIntent);
return false;
}
});
}
/**
@@ -582,7 +567,10 @@ public class SettingsActivity extends PreferenceActivity {
List<String> entryValues = new ArrayList<String>();
// 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[] entryValuesArray = entryValues.toArray(new CharSequence[entryValues.size()]);
@@ -685,7 +673,7 @@ public class SettingsActivity extends PreferenceActivity {
SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
Editor editor = prefs.edit();
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.Waypoint;
import com.google.android.apps.mytracks.services.ServiceUtils;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerFactory;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.android.maps.mytracks.R;
import android.app.Activity;
@@ -32,6 +31,7 @@ import android.content.Context;
import android.content.SharedPreferences;
import android.location.Location;
import android.os.Bundle;
import android.speech.tts.TextToSpeech;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Window;
@@ -105,9 +105,7 @@ public class StatsActivity extends Activity implements TrackDataListener {
utils = new StatsUtilities(this);
// The volume we want to control is the Text-To-Speech volume
int volumeStream =
new StatusAnnouncerFactory(ApiFeatures.getInstance()).getVolumeStream();
setVolumeControlStream(volumeStream);
setVolumeControlStream(TextToSpeech.Engine.DEFAULT_STREAM);
// We don't need a window title bar:
requestWindowFeature(Window.FEATURE_NO_TITLE);
@@ -286,6 +286,17 @@ public class TrackList extends ListActivity
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() {
View deleteAll = findViewById(R.id.tracklist_btn_delete_all);
View exportAll = findViewById(R.id.tracklist_btn_export_all);
@@ -198,6 +198,17 @@ public class WaypointsList extends ListActivity
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
public void onClick(View v) {
WaypointCreationRequest request;
@@ -266,10 +277,10 @@ public class WaypointsList extends ListActivity
if (columnIndex == timeIdx) {
long time = cursor.getLong(timeIdx);
TextView textView = (TextView) view;
if (time == 0) {
textView.setVisibility(View.GONE);
} else {
} else {
textView.setText(StringUtils.formatDateTime(WaypointsList.this, time));
textView.setVisibility(View.VISIBLE);
}
@@ -16,7 +16,6 @@
package com.google.android.apps.mytracks.content;
import com.google.android.apps.mytracks.Constants;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.ContentProvider;
@@ -417,12 +416,9 @@ public class MyTracksProvider extends ContentProvider {
} else {
throw new IllegalArgumentException("Unknown URL " + url);
}
if (ApiFeatures.getInstance().canReuseSQLiteQueryBuilder()) {
Log.i(Constants.TAG,
"Build query: " + 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,
sortOrder);
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.TrackDataListener.ProviderState;
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.maps.mytracks.R;
@@ -196,7 +196,7 @@ public class TrackDataHub {
private DoubleBufferedLocationFactory locationFactory;
private boolean started = false;
/**
* Builds a new {@link TrackDataHub} instance.
*/
@@ -274,7 +274,7 @@ public class TrackDataHub {
Log.w(TAG, "Not started, ignoring");
return;
}
// Unregister internal listeners even if there are external listeners registered.
dataSourceManager.unregisterAllListeners();
listenerHandlerThread.getLooper().quit();
@@ -399,7 +399,7 @@ public class TrackDataHub {
// Save the selection to memory and flush.
selectedTrackId = trackId;
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(
preferences.edit().putLong(SELECTED_TRACK_KEY, trackId));
// Force it to reload data from the beginning.
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.backup;
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.Editor;
@@ -106,7 +106,7 @@ class PreferenceBackupHelper {
byte typeId = reader.readByte();
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.io.sendtogoogle.SendToGoogleUtils;
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.StringUtils;
import com.google.android.apps.mytracks.util.SystemUtils;
@@ -109,7 +109,7 @@ public class SendFusionTablesAsyncTask extends AsyncTask<Void, Integer, Boolean>
context = activity.getApplicationContext();
myTracksProviderUtils = MyTracksProviderUtils.Factory.get(context);
HttpTransport transport = ApiFeatures.getInstance().getApiAdapter().getHttpTransport();
HttpTransport transport = ApiAdapterFactory.getApiAdapter().getHttpTransport();
httpRequestFactory = transport.createRequestFactory(new MethodOverride());
canRetry = true;
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
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 android.accounts.Account;
@@ -153,7 +153,7 @@ public class AccountChooserActivity extends Activity {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
Editor editor = prefs.edit();
editor.putString(getString(R.string.preferred_account_key), account.name);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
Intent intent = new Intent();
intent.putExtra(ACCOUNT, account);
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.io.sendtogoogle;
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 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_docs_key), sendDocs());
}
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
/**
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services;
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 android.content.Context;
@@ -174,19 +174,19 @@ public class PreferenceManager implements OnSharedPreferenceChangeListener {
public void setAutoResumeTrackCurrentRetry(int retryAttempts) {
Editor editor = sharedPreferences.edit();
editor.putInt(autoResumeTrackCurrentRetryKey, retryAttempts);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setRecordingTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(recordingTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public void setSelectedTrack(long id) {
Editor editor = sharedPreferences.edit();
editor.putLong(selectedTrackKey, id);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
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.stats.TripStatistics;
import com.google.android.apps.mytracks.stats.TripStatisticsBuilder;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.apps.mytracks.util.ApiLevelAdapter;
import com.google.android.apps.mytracks.util.LocationUtils;
import com.google.android.apps.mytracks.util.StringUtils;
import com.google.android.maps.mytracks.R;
import com.google.common.annotations.VisibleForTesting;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.ContentValues;
@@ -80,7 +78,6 @@ public class TrackRecordingService extends Service {
static final int MAX_AUTO_RESUME_TRACK_RETRY_ATTEMPTS = 3;
private NotificationManager notificationManager;
private LocationManager locationManager;
private WakeLock wakeLock;
@@ -243,8 +240,6 @@ public class TrackRecordingService extends Service {
super.onCreate();
Log.d(TAG, "TrackRecordingService.onCreate");
providerUtils = MyTracksProviderUtils.Factory.get(this);
notificationManager =
(NotificationManager) getSystemService(NOTIFICATION_SERVICE);
locationManager = (LocationManager) getSystemService(LOCATION_SERVICE);
setUpTaskExecutors();
@@ -369,7 +364,6 @@ public class TrackRecordingService extends Service {
// Make sure we have no indirect references to this service.
locationManager = null;
notificationManager = null;
providerUtils = null;
binder.detachFromService();
binder = null;
@@ -485,8 +479,6 @@ public class TrackRecordingService extends Service {
* Shows the notification message and icon in the notification bar.
*/
private void showNotification() {
final ApiLevelAdapter apiLevelAdapter =
ApiFeatures.getInstance().getApiAdapter();
if (isRecording) {
Notification notification = new Notification(
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),
getString(R.string.track_record_notification), contentIntent);
notification.flags += Notification.FLAG_NO_CLEAR;
apiLevelAdapter.startForeground(this, notificationManager, 1,
notification);
startForegroundService(notification);
} 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() {
announcementExecutor = new PeriodicTaskExecutor(
this, new StatusAnnouncerFactory(ApiFeatures.getInstance()));
announcementExecutor = new PeriodicTaskExecutor(this, new StatusAnnouncerFactory());
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.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.BluetoothDevice;
@@ -31,8 +31,6 @@ import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
/**
@@ -44,8 +42,9 @@ import java.util.UUID;
* @author Sandor Dornbush
*/
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;
@@ -254,41 +253,13 @@ public class BluetoothConnectionManager {
// Get a BluetoothSocket for a connection with the
// given BluetoothDevice
try {
tmp = getSocket();
tmp = ApiAdapterFactory.getApiAdapter().getBluetoothSocket(device);
} catch (IOException e) {
Log.e(Constants.TAG, "create() failed", e);
}
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
public void run() {
Log.d(Constants.TAG, "BEGIN mConnectThread");
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.services.sensors;
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;
@@ -62,8 +62,7 @@ public class ZephyrMessageParser implements MessageParser {
// 0x1A00316550003162 produces erroneous values for Cadence and needs
// a workaround based on the stride counter.
// Firmware values range from field 3 to 10 (inclusive) of the byte buffer.
byte[] hardwareFirmwareId = ApiFeatures.getInstance().getApiAdapter()
.copyByteArray(buffer, 3, 11);
byte[] hardwareFirmwareId = ApiAdapterFactory.getApiAdapter().copyByteArray(buffer, 3, 11);
Sensor.SensorData.Builder cadence = Sensor.SensorData.newBuilder();
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.google.android.apps.mytracks.Constants;
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 android.content.Context;
@@ -135,7 +135,7 @@ public class AntDirectSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
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) {
@@ -22,7 +22,7 @@ import com.dsi.ant.AntMesg;
import com.dsi.ant.exception.AntInterfaceException;
import com.google.android.apps.mytracks.Constants;
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.maps.mytracks.R;
@@ -199,7 +199,7 @@ public class AntSrmBridgeSensorManager extends AntSensorManager {
Constants.SETTINGS_NAME, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
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) {
@@ -15,10 +15,9 @@
*/
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.media.AudioManager;
/**
* 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 {
private final boolean hasTts;
public StatusAnnouncerFactory(ApiFeatures apiFeatures) {
this.hasTts = apiFeatures.hasTextToSpeech();
public StatusAnnouncerFactory() {
}
@Override
public PeriodicTask create(Context context) {
if (hasTts) {
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;
}
return ApiAdapterFactory.getApiAdapter().getStatusAnnouncerTask(context);
}
}
@@ -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.
*
@@ -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;
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.services.sensors.BluetoothConnectionManager;
import com.google.android.apps.mytracks.services.tasks.PeriodicTask;
import com.google.android.apps.mytracks.services.tasks.StatusAnnouncerTask;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.http.apache.ApacheHttpTransport;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.util.Log;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
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
*/
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
public PeriodicTask getStatusAnnouncerTask(Context 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
public void applyPreferenceChanges(Editor editor) {
editor.commit();
@@ -114,4 +78,25 @@ public class ApiLevel3Adapter implements ApiLevelAdapter {
public HttpTransport getHttpTransport() {
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;
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;
/**
* API level 8 specific implementation of the {@link ApiLevelAdapter}.
* API level 8 specific implementation of the {@link ApiAdapter}.
*
* @author Jimmy Shih
*/
public class ApiLevel8Adapter extends ApiLevel5Adapter {
public class Api8Adapter extends Api7Adapter {
@Override
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;
import com.google.android.apps.mytracks.Constants;
@@ -11,11 +26,11 @@ import android.util.Log;
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
*/
public class ApiLevel9Adapter extends ApiLevel8Adapter {
public class Api9Adapter extends Api8Adapter {
@Override
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;
import com.google.android.apps.mytracks.Constants;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.os.Build;
import android.util.Log;
import java.util.List;
import java.util.Set;
/**
* 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
*/
public abstract class BluetoothDeviceUtils {
public static final String ANY_DEVICE = "any";
private static BluetoothDeviceUtils instance;
public class BluetoothDeviceUtils {
private BluetoothDeviceUtils() {}
/**
* Dummy implementation, for systems that don't support bluetooth.
*/
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
* Populates the device names and the device addresses with all the suitable
* bluetooth devices.
*
* @param deviceNames the list to populate with user-visible names
* @param deviceAddresses the list to populate with device addresses
* @param bluetoothAdapter the bluetooth adapter
* @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();
/**
* Finds the bluetooth device with the given address.
*
* @param targetDeviceAddress the address of the device, or
* {@link #ANY_DEVICE} for using the first suitable device
* @return the device's descriptor, or null if not found
*/
public abstract BluetoothDevice findDeviceMatching(String targetDeviceAddress);
/**
* @return whether the bluetooth method is supported on this device
*/
public static boolean isBluetoothMethodSupported() {
return Integer.parseInt(Build.VERSION.SDK) >= 5;
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());
}
}
}
}
}
@@ -45,7 +45,7 @@ public class EulaUtil {
EULA_PREFERENCE_FILE, Context.MODE_PRIVATE);
Editor editor = preferences.edit();
editor.putBoolean(EULA_PREFERENCE_KEY, true);
ApiFeatures.getInstance().getApiAdapter().applyPreferenceChanges(editor);
ApiAdapterFactory.getApiAdapter().applyPreferenceChanges(editor);
}
public static String getEulaMessage(Context context) {
@@ -1,12 +1,12 @@
/*
* 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
@@ -154,7 +154,7 @@ public interface MyTracksProviderUtils {
/**
* Creates a cursor over the locations in the track points provider which
* 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 minTrackPointId the minimum id for the track points
@@ -168,7 +168,7 @@ public interface MyTracksProviderUtils {
/**
* 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 minWaypointId the minimum id for the track points
@@ -178,6 +178,18 @@ public interface MyTracksProviderUtils {
Cursor getWaypointsCursor(long trackId, long minWaypointId,
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.
* 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
*/
Track getTrack(long id);
/**
* Retrieves all tracks without track points. If no tracks exist, an empty
* list will be returned. Use {@link #getLocationIterator(long, long, boolean, LocationFactory)}
* to load the track points.
*
*
* @return a list of all the recorded tracks
*/
List<Track> getAllTracks();
/**
* 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 selecttionArgs parameters for the given selection
* @param order the order to return results in
* @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.
@@ -288,7 +302,7 @@ public interface MyTracksProviderUtils {
/**
* Fill a location object with values from a given cursor.
*
*
* @param cursor a cursor pointing at a db or provider with locations
* @param location a location object to be overwritten
*/
@@ -308,7 +322,7 @@ public interface MyTracksProviderUtils {
interface LocationIterator extends Iterator<Location> {
/**
* 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.
*/
long getLocationId();
@@ -328,12 +342,12 @@ public interface MyTracksProviderUtils {
* 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
* existing to optimize for speed.
*
*
* @return a {@link Location} to be populated from the database.
*/
Location createLocation();
}
/**
* The default {@class Location}s factory, which creates a new location of 'gps' type.
*/
@@ -361,20 +375,20 @@ public interface MyTracksProviderUtils {
return locs[lastLoc];
}
}
/**
* 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
* limitations. Since it's a read-only iterator, {@link Iterator#remove()} always throws
* {@class UnsupportedOperationException}.
*
*
* 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
* retrieved from the record.
*
*
* When done with iteration, you must call {@link LocationIterator#close()} to make sure that all
* resources are properly deallocated.
*
*
* Example use:
* <code>
* ...
@@ -389,14 +403,14 @@ public interface MyTracksProviderUtils {
* }
* ...
* </code>
*
*
* @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
* the first point.
* @param descending if true the results will be returned in descending ID
* order (latest location first).
* @param locationFactory the factory for creating new locations.
*
*
* @return the read-only iterator over the given track's points.
*/
LocationIterator getLocationIterator(long trackId, long startTrackPointId, boolean descending,
@@ -16,7 +16,7 @@
package com.google.android.apps.mytracks.content;
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.protobuf.InvalidProtocolBufferException;
@@ -875,22 +875,34 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
}
String selection;
String[] selectionArgs;
if (minWaypointId > 0) {
selection = String.format("%s=%d AND %s>=%d",
WaypointsColumns.TRACKID, trackId,
WaypointsColumns._ID, minWaypointId);
selection = String.format("%s = ? AND %s >= ?",
WaypointsColumns.TRACKID,
WaypointsColumns._ID);
selectionArgs = new String[] {
Long.toString(trackId),
Long.toString(minWaypointId)
};
} else {
selection = String.format("%s=%d",
WaypointsColumns.TRACKID, trackId);
selection = String.format("%s=?", WaypointsColumns.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) {
sortOrder += " LIMIT " + maxWaypoints;
order += " LIMIT " + maxWaypoints;
}
return contentResolver.query(
WaypointsColumns.CONTENT_URI, null, selection, null, sortOrder);
WaypointsColumns.CONTENT_URI, null, selection, selectionArgs, order);
}
@Override
@@ -905,7 +917,7 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
@Override
public List<Track> getAllTracks() {
Cursor cursor = getTracksCursor(null);
Cursor cursor = getTracksCursor(null, null, TracksColumns._ID);
ArrayList<Track> tracks = new ArrayList<Track>();
if (cursor != null) {
tracks.ensureCapacity(cursor.getCount());
@@ -923,10 +935,9 @@ public class MyTracksProviderUtilsImpl implements MyTracksProviderUtils {
}
@Override
public Cursor getTracksCursor(String selection) {
Cursor cursor = contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, null, "_id");
return cursor;
public Cursor getTracksCursor(String selection, String[] selectionArgs, String order) {
return contentResolver.query(
TracksColumns.CONTENT_URI, null, selection, selectionArgs, order);
}
@Override
@@ -1,12 +1,12 @@
/*
* 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
@@ -26,7 +26,7 @@ import android.os.Parcelable;
*
* @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
@@ -144,7 +144,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the time that this track started.
*
*
* @return The number of milliseconds since epoch to the time when this track
* started
*/
@@ -154,7 +154,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the time that this track stopped.
*
*
* @return The number of milliseconds since epoch to the time when this track
* stopped
*/
@@ -176,7 +176,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the total distance the user traveled.
*
*
* @return The total distance traveled in meters
*/
public double getTotalDistance() {
@@ -197,7 +197,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the the average speed the user traveled when they were actively
* moving.
*
*
* @return The average moving speed in m/s
*/
public double getAverageMovingSpeed() {
@@ -206,7 +206,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the the maximum speed for this track.
*
*
* @return The maximum speed in m/s
*/
public double getMaxSpeed() {
@@ -215,7 +215,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the moving time.
*
*
* @return The total number of milliseconds the user was moving
*/
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
* of all positive differences in the smoothed elevation.
*
*
* @return The elevation gain in meters for this trip
*/
public double getTotalElevationGain() {
@@ -233,39 +233,79 @@ public class TripStatistics implements Parcelable {
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed
* decimal degrees.
* Returns the leftmost position (lowest longitude) of the track, in signed degrees.
*/
public double getLeftDegrees() {
return longitudeExtremities.getMin();
}
/**
* Returns the leftmost position (lowest longitude) of the track, in signed millions of degrees.
*/
public int getLeft() {
return (int) (longitudeExtremities.getMin() * 1E6);
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed
* decimal degrees.
* Returns the rightmost position (highest longitude) of the track, in signed degrees.
*/
public double getRightDegrees() {
return longitudeExtremities.getMax();
}
/**
* Returns the rightmost position (highest longitude) of the track, in signed millions of degrees.
*/
public int getRight() {
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() {
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() {
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
* smoothed elevation so this can actually be more than the current elevation.
*
*
* @return The smallest elevation reading for this trip in meters
*/
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
* smoothed elevation so this can actually be less than the current elevation.
*
*
* @return The largest elevation reading for this trip in meters
*/
public double getMaxElevation() {
@@ -284,7 +324,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the maximum grade for this trip.
*
*
* @return The maximum grade for this trip as a fraction
*/
public double getMaxGrade() {
@@ -293,7 +333,7 @@ public class TripStatistics implements Parcelable {
/**
* Gets the minimum grade for this trip.
*
*
* @return The minimum grade for this trip as a fraction
*/
public double getMinGrade() {
@@ -443,7 +483,7 @@ public class TripStatistics implements Parcelable {
void addMovingTime(long time) {
movingTime += time;
}
/**
* 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.WaypointCreationRequest;
import com.google.android.apps.mytracks.stats.TripStatistics;
import com.google.android.apps.mytracks.util.ApiFeatures;
import com.google.android.maps.mytracks.R;
import android.content.BroadcastReceiver;
@@ -57,8 +56,7 @@ import java.util.List;
* is called, which prevents from testing multiple scenarios in a single
* test (see runFunctionTest for more details).
*/
public class TrackRecordingServiceTest
extends ServiceTestCase<TrackRecordingService> {
public class TrackRecordingServiceTest extends ServiceTestCase<TestRecordingService> {
private Context context;
private MyTracksProviderUtils providerUtils;
@@ -74,7 +72,7 @@ public class TrackRecordingServiceTest
private Intent serviceIntent;
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
protected IBinder bindService(Intent intent) {
if (getService() != null) {
@@ -138,8 +123,6 @@ public class TrackRecordingServiceTest
protected void setUp() throws Exception {
super.setUp();
ApiFeatures.injectInstance(new MockApiFeatures());
MockContentResolver mockContentResolver = new MockContentResolver();
RenamingDelegatingContext targetContext = new RenamingDelegatingContext(
getContext(), getContext(), "test.");
@@ -15,13 +15,6 @@
*/
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;
/**
@@ -31,62 +24,9 @@ import android.test.AndroidTestCase;
* @author Rodrigo Damazio
*/
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() {
apiFeatures.setHasTextToSpeech(true);
PeriodicTaskFactory factory =
new StatusAnnouncerFactory(apiFeatures);
PeriodicTaskFactory factory = new StatusAnnouncerFactory();
PeriodicTask task = factory.create(getContext());
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;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.Service;
import junit.framework.TestCase;
/**
* API level 5 specific implementation of the {@link ApiLevelAdapter}.
* Tests {@link ApiAdapterFactory}.
*
* @author Bartlomiej Niechwiej
* @author Rodrigo Damazio
*/
public class ApiLevel5Adapter extends ApiLevel3Adapter {
@Override
public void startForeground(Service service,
NotificationManager notificationManager, int id,
Notification notification) {
service.startForeground(id, notification);
}
@Override
public void stopForeground(Service service,
NotificationManager notificationManager, int id) {
service.stopForeground(id != -1);
public class ApiAdapterFactoryTest extends TestCase {
public void testGetApiLevelAdapter() {
assertNotNull(ApiAdapterFactory.getApiAdapter());
}
}
@@ -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());
}
}