diff --git a/MyTracks/AndroidManifest.xml b/MyTracks/AndroidManifest.xml index 90cac25bf..aa8f0a68d 100755 --- a/MyTracks/AndroidManifest.xml +++ b/MyTracks/AndroidManifest.xml @@ -112,12 +112,19 @@ limitations under the License. + + + + + @@ -129,7 +136,12 @@ limitations under the License. + + + + + + + - - + + + + + + - + + + + @@ -168,7 +193,6 @@ limitations under the License. - @@ -191,6 +215,10 @@ limitations under the License. + + + + + + + + + + + + + - + diff --git a/MyTracks/res/menu/main.xml b/MyTracks/res/menu/main.xml index 7979d052b..21c80178c 100644 --- a/MyTracks/res/menu/main.xml +++ b/MyTracks/res/menu/main.xml @@ -1,4 +1,19 @@ + + + - + - + - + + + + + \ No newline at end of file diff --git a/MyTracks/res/values/strings.xml b/MyTracks/res/values/strings.xml index ba3b892dd..1da6821c9 100644 --- a/MyTracks/res/values/strings.xml +++ b/MyTracks/res/values/strings.xml @@ -935,6 +935,10 @@ limitations under the License. Screenshot: menu_record_track.png"> Tracks + + Search + + + + Search My Tracks tracks and waypoints + + + My Tracks tracks and waypoints + + + Speak the name of a track or waypoint. + + Voice - - Voice announcement is not available on your Android phone. - + \ No newline at end of file diff --git a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java index fbc24fcdf..ddebba571 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ChartActivity.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java index 38b52f3e2..e56ca4327 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java +++ b/MyTracks/src/com/google/android/apps/mytracks/CheckUnits.java @@ -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)); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java index a4a96cfa5..776f4bbd2 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteAllTracks.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java index a1d26a6c3..e34d78b49 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java +++ b/MyTracks/src/com/google/android/apps/mytracks/DeleteTrack.java @@ -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); } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java index b90d66d41..ef6169658 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/ExportAllTracks.java @@ -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; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java index 21524287a..b5a80f510 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MapActivity.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java index 468680efd..52dcf2c25 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MenuManager.java @@ -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; } diff --git a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java index e22dc054a..1ab8466b9 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java +++ b/MyTracks/src/com/google/android/apps/mytracks/MyTracks.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java new file mode 100644 index 000000000..d4ec2ffb1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/SearchActivity.java @@ -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 scoredResults = engine.search(query); + + final List> 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> prepareResultsforDisplay( + Collection scoredResults) { + ArrayList> output = new ArrayList>(scoredResults.size()); + for (ScoredResult result : scoredResults) { + Map resultMap = new HashMap(); + 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 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 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> 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 clickedData = (Map) getListAdapter().getItem(position); + + startActivity(createViewDataIntent(clickedData)); + } + + private Intent createViewDataIntent(Map 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; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java index 45a872cb9..a61fd744d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/SettingsActivity.java @@ -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 entryValues = new ArrayList(); // 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); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java index 2c705fb29..adb62c2d1 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/StatsActivity.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java index 94166448a..25441b427 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/TrackList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/TrackList.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java index a9648de6c..c2752f167 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java +++ b/MyTracks/src/com/google/android/apps/mytracks/WaypointsList.java @@ -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); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java index bba0b5448..2f959a575 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/MyTracksProvider.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java new file mode 100644 index 000000000..b47f3ea8f --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngine.java @@ -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 SCORED_RESULT_COMPARATOR = + new Comparator() { + @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 search(SearchQuery query) { + ArrayList tracks = new ArrayList(); + ArrayList waypoints = new ArrayList(); + TreeSet scoredResults = new TreeSet(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 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 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 tracks, SearchQuery query, Collection 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 waypoints, SearchQuery query, Collection 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); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java new file mode 100644 index 000000000..ae7c64fe1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/content/SearchEngineProvider.java @@ -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); + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java index b4968f680..4060acff8 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java +++ b/MyTracks/src/com/google/android/apps/mytracks/content/TrackDataHub.java @@ -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. diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java index 4a3cc2738..1b70dc1f5 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/backup/PreferenceBackupHelper.java @@ -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); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java index c53345eac..3e271e2f4 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/fusiontables/SendFusionTablesAsyncTask.java @@ -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 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; diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java index a6e22bafe..4574dbe83 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/AccountChooserActivity.java @@ -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); diff --git a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java index b6f3fc8d1..da27228bf 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java +++ b/MyTracks/src/com/google/android/apps/mytracks/io/sendtogoogle/UploadServiceChooserActivity.java @@ -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); } /** diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java index 501fadf39..9e9c90a35 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/PreferenceManager.java @@ -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() { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java index ee1e021cc..8b81a44df 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/TrackRecordingService.java @@ -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()); } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java index 4a3bc512b..558bfe423 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/BluetoothConnectionManager.java @@ -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 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"); diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java index b70e0fddf..a8354ee95 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ZephyrMessageParser.java @@ -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)) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java index 4c0db0864..e72635f79 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntDirectSensorManager.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java index 1db754b4d..1006e4a6b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/sensors/ant/AntSrmBridgeSensorManager.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java index 739b81cb3..7e8b56b37 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactory.java @@ -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); } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java index f6532249b..24bbd674b 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java +++ b/MyTracks/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerTask.java @@ -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. * diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java new file mode 100644 index 000000000..8bdece372 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api10Adapter.java @@ -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); + }; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java similarity index 63% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java index dcd033e14..6b572d045 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel3Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api7Adapter.java @@ -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 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); + } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java similarity index 50% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java index 35696189d..f8371e081 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel8Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api8Adapter.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java similarity index 61% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java rename to MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java index 5ab6ab0aa..86fb12d3d 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel9Adapter.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/Api9Adapter.java @@ -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) { diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java new file mode 100644 index 000000000..868c43af1 --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapter.java @@ -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. + *

+ * Due to changes in API level 8. + * + * @param context the context + */ + public PeriodicTask getStatusAnnouncerTask(Context context); + + /** + * Gets a {@link BackupPreferencesListener}. + *

+ * 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. + *

+ * 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. + *

+ * 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. + *

+ * 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}. + *

+ * Due to changes in API level 9. + */ + public HttpTransport getHttpTransport(); + + /** + * Gets a {@link BluetoothSocket}. + *

+ * Due to changes in API level 10. + * + * @param bluetoothDevice + */ + public BluetoothSocket getBluetoothSocket(BluetoothDevice bluetoothDevice) throws IOException; +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java new file mode 100644 index 000000000..5af16534e --- /dev/null +++ b/MyTracks/src/com/google/android/apps/mytracks/util/ApiAdapterFactory.java @@ -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; + } +} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java deleted file mode 100644 index 700b5a2a6..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiFeatures.java +++ /dev/null @@ -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; - } -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java b/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java deleted file mode 100644 index f2180dfc2..000000000 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevelAdapter.java +++ /dev/null @@ -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(); -} diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java index 96e8bd878..548d873c7 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/BluetoothDeviceUtils.java @@ -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 deviceNames, List 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 deviceNames, List deviceAddresses) { - ensureNotDiscovering(); - - Set 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 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 deviceNames, List deviceAddresses); + public static void populateDeviceLists( + BluetoothAdapter bluetoothAdapter, List deviceNames, List 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 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()); + } + } + } } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java b/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java index 0008b6e8e..6d2253844 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java +++ b/MyTracks/src/com/google/android/apps/mytracks/util/EulaUtil.java @@ -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) { diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java index a9506f1fc..87d04eef8 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtils.java @@ -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 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 { /** * 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: * * ... @@ -389,14 +403,14 @@ public interface MyTracksProviderUtils { * } * ... * - * + * * @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, diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java index 44ff1891a..ee4cc81b6 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/content/MyTracksProviderUtilsImpl.java @@ -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 getAllTracks() { - Cursor cursor = getTracksCursor(null); + Cursor cursor = getTracksCursor(null, null, TracksColumns._ID); ArrayList tracks = new ArrayList(); 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 diff --git a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java index 09508bf41..dd318095e 100644 --- a/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java +++ b/MyTracksLib/src/com/google/android/apps/mytracks/stats/TripStatistics.java @@ -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. * diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java new file mode 100644 index 000000000..3fef63c1d --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/content/SearchEngineTest.java @@ -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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 results = new ArrayList(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 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 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()); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java new file mode 100644 index 000000000..2eaa20aad --- /dev/null +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TestRecordingService.java @@ -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)}. + *

+ * See http://code.google.com/p/android/issues/detail?id=12122 + *

+ * 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); + } + } +} diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java index 25fe512ef..df0b079a3 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/TrackRecordingServiceTest.java @@ -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 { +public class TrackRecordingServiceTest extends ServiceTestCase { 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."); diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java index ea9a8d441..91d9b3eeb 100644 --- a/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/services/tasks/StatusAnnouncerFactoryTest.java @@ -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()); - } } diff --git a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java similarity index 51% rename from MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java rename to MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java index 0f963f70f..4b73a6a6f 100644 --- a/MyTracks/src/com/google/android/apps/mytracks/util/ApiLevel5Adapter.java +++ b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiAdapterFactoryTest.java @@ -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()); } } diff --git a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java b/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java deleted file mode 100644 index 5b08a975f..000000000 --- a/MyTracksTest/src/com/google/android/apps/mytracks/util/ApiFeaturesTest.java +++ /dev/null @@ -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()); - } -}