From 9ef312fc933df1ca9a786c53d3366ba9f30aef0b Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Tue, 3 Nov 2020 18:05:29 +0100 Subject: [PATCH] Search is now handled by TrackListActivity and MarkerListActivity. * data is loaded via cursor, * removed custom ranking (handled in database), * search text is wrapped with wildcards, and * search view can be closed by back button. Fixes #492. --- doc/activities.txt | 19 +- .../opentracks/content/SearchEngineTest.java | 323 ------------- src/main/AndroidManifest.xml | 28 +- .../opentracks/AbstractListActivity.java | 7 +- .../opentracks/MarkerDetailActivity.java | 2 +- .../opentracks/MarkerListActivity.java | 129 +++-- .../opentracks/SearchListActivity.java | 446 ------------------ .../opentracks/TrackListActivity.java | 135 ++++-- .../opentracks/TrackRecordedActivity.java | 2 +- .../opentracks/content/SearchEngine.java | 425 ----------------- .../provider/ContentProviderUtils.java | 32 +- .../fragments/DeleteMarkerDialogFragment.java | 4 +- .../io/file/exporter/ExportActivity.java | 1 - .../opentracks/util/ActivityUtils.java | 6 +- .../opentracks/util/TrackUtils.java | 3 - src/main/res/layout/search_list.xml | 32 -- src/main/res/xml/searchable.xml | 4 +- 17 files changed, 228 insertions(+), 1370 deletions(-) delete mode 100644 src/androidTest/java/de/dennisguse/opentracks/content/SearchEngineTest.java delete mode 100644 src/main/java/de/dennisguse/opentracks/SearchListActivity.java delete mode 100644 src/main/java/de/dennisguse/opentracks/content/SearchEngine.java delete mode 100644 src/main/res/layout/search_list.xml diff --git a/doc/activities.txt b/doc/activities.txt index f385ac47a..effa3e71c 100644 --- a/doc/activities.txt +++ b/doc/activities.txt @@ -10,15 +10,11 @@ endlegend [*] --> TrackListActivity TrackListActivity --> TrackRecordingActivity : Start recording -TrackListActivity --> SearchListActivity +TrackListActivity --> TrackRecordedActivity : Show track TrackListActivity --> MarkerListActivity : Show all markers TrackListActivity --> AggregatedStatisticsActivity : Aggregated stats -TrackListActivity --> TrackRecordedActivity : Show track TrackListActivity --> TrackDeleteActivity -SearchListActivity --> MarkerDetailActivity : Search result -SearchListActivity --> TrackRecordedActivity : Search result - TrackRecordedActivity --> TrackDeleteActivity TrackRecordedActivity --> TrackEditActivity TrackRecordedActivity --> MarkerListActivity @@ -46,11 +42,14 @@ MarkerDetailActivity --> CoordinateOnMapIntent : URI SettingsActivity --> AboutActivity SettingsActivity --> HelpActivity +SettingsActivity --> ImportActivity SettingsActivity --> ExportActivity -SettingsActivity --> DirectoryChooserActivity$ImportDirectoryChooserActivity -SettingsActivity --> DirectoryChooserActivity$ExportDirectoryChooserActivity -SettingsActivity --> DirectoryChooserActivity$DefaultTrackExportDirectoryChooserActivity -DirectoryChooserActivity$ImportDirectoryChooserActivity --> ImportActivity -DirectoryChooserActivity$ExportDirectoryChooserActivity --> ExportActivity +SettingsActivity --> DirectoryChooserActivity.ImportDirectoryChooserActivity +SettingsActivity --> DirectoryChooserActivity.ExportDirectoryChooserActivity +SettingsActivity --> DirectoryChooserActivity.DefaultTrackExportDirectoryChooserActivity +DirectoryChooserActivity.ImportDirectoryChooserActivity --> ImportActivity +DirectoryChooserActivity.ExportDirectoryChooserActivity --> ExportActivity + +FileImportIntent --> ImportActivity @enduml diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/SearchEngineTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/SearchEngineTest.java deleted file mode 100644 index ff2b4df88..000000000 --- a/src/androidTest/java/de/dennisguse/opentracks/content/SearchEngineTest.java +++ /dev/null @@ -1,323 +0,0 @@ -/* - * 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 de.dennisguse.opentracks.content; - -import android.content.ContentUris; -import android.location.Location; -import android.net.Uri; - -import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.rule.provider.ProviderTestRule; - -import org.junit.After; -import org.junit.Before; -import org.junit.Rule; -import org.junit.Test; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import de.dennisguse.opentracks.content.SearchEngine.ScoredResult; -import de.dennisguse.opentracks.content.SearchEngine.SearchQuery; -import de.dennisguse.opentracks.content.data.Marker; -import de.dennisguse.opentracks.content.data.Track; -import de.dennisguse.opentracks.content.provider.ContentProviderUtils; -import de.dennisguse.opentracks.content.provider.CustomContentProvider; -import de.dennisguse.opentracks.content.provider.CustomSQLiteOpenHelper; -import de.dennisguse.opentracks.stats.TrackStatistics; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - -/** - * 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 { - - private static final String TAG = SearchEngineTest.class.getSimpleName(); - - private static final Location HERE = new Location("gps"); - private static final long NOW = SearchEngine.OLDEST_ALLOWED_TIMESTAMP + 1000; - - @Rule - public ProviderTestRule sqliteContentProviderRule = new ProviderTestRule.Builder(CustomContentProvider.class, ContentProviderUtils.AUTHORITY_PACKAGE).setPrefix(TAG).build(); - - private ContentProviderUtils providerUtils; - private SearchEngine engine; - - @Before - public void setUp() { - providerUtils = new ContentProviderUtils(sqliteContentProviderRule.getResolver()); - - engine = new SearchEngine(providerUtils); - } - - @After - public void tearDown() { - InstrumentationRegistry.getInstrumentation().getTargetContext().deleteDatabase(TAG + CustomSQLiteOpenHelper.DATABASE_NAME); - } - - private Track.Id insertTrack(String title, String description, String category, long hoursAgo) { - Track track = new Track(); - track.setName(title); - track.setDescription(description); - track.setCategory(category); - - TrackStatistics stats = track.getTrackStatistics(); - if (hoursAgo > 0) { - // Started twice hoursAgo, so the average time is hoursAgo. - stats.setStartTime_ms(NOW - hoursAgo * 1000L * 60L * 60L * 2); - stats.setStopTime_ms(NOW); - } - - Uri uri = providerUtils.insertTrack(track); - return new Track.Id(ContentUris.parseId(uri)); - } - - private Track.Id insertTrack(String title, String description, String category) { - return insertTrack(title, description, category, -1); - } - - private Track.Id insertTrack(String title, long hoursAgo) { - return insertTrack(title, "", "", hoursAgo); - } - - private Marker.Id insertWaypoint(String title, String description, String category, double distance, long hoursAgo, Track.Id 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); - } - Marker waypoint = new Marker(trackId, location); - waypoint.setName(title); - waypoint.setDescription(description); - waypoint.setCategory(category); - - Uri uri = providerUtils.insertMarker(waypoint); - return new Marker.Id(ContentUris.parseId(uri)); - } - - private Marker.Id insertWaypoint(String title, String description, String category, Track.Id trackId) { - return insertWaypoint(title, description, category, 0.0, -1, trackId); - } - - private Marker.Id insertWaypoint(String title, double distance, Track.Id trackId) { - return insertWaypoint(title, "", "", distance, -1, trackId); - } - - private Marker.Id insertWaypoint(String title, long hoursAgo, Track.Id trackId) { - return insertWaypoint(title, "", "", 0.0, hoursAgo, trackId); - } - - @Test - 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"); - Track.Id descriptionMatchId = insertTrack("bb", "aa", "cc"); - Track.Id categoryMatchId = insertTrack("bb", "cc", "aa"); - Track.Id titleMatchId = insertTrack("aa", "bb", "cc"); - Track.Id titleCategoryMatchId = insertTrack("aa", "bb", "ca"); - Track.Id titleDescriptionMatchId = insertTrack("aa", "ba", "cc"); - Track.Id allMatchId = insertTrack("aa", "ba", "ca"); - - SearchQuery query = new SearchQuery("a", null, null, NOW); - ArrayList results = new ArrayList<>(engine.search(query)); - - // Title > Description > Category. - assertTrackResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId); - } - - @Test - 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 - Track.Id trackId = insertTrack("", "", ""); - insertWaypoint("bb", "cc", "dd", trackId); - Marker.Id descriptionMatchId = insertWaypoint("bb", "aa", "cc", trackId); - Marker.Id categoryMatchId = insertWaypoint("bb", "cc", "aa", trackId); - Marker.Id titleMatchId = insertWaypoint("aa", "bb", "cc", trackId); - Marker.Id titleCategoryMatchId = insertWaypoint("aa", "bb", "ca", trackId); - Marker.Id titleDescriptionMatchId = insertWaypoint("aa", "ba", "cc", trackId); - Marker.Id allMatchId = insertWaypoint("aa", "ba", "ca", trackId); - - SearchQuery query = new SearchQuery("a", null, null, NOW); - ArrayList results = new ArrayList<>(engine.search(query)); - - // Title > Description > Category. - assertWaypointResults(results, allMatchId, titleDescriptionMatchId, titleCategoryMatchId, titleMatchId, descriptionMatchId, categoryMatchId); - } - - @Test - 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 - Track.Id trackId = insertTrack("aa", "bb", "cc"); - Marker.Id descriptionWaypointId = insertWaypoint("bb", "aa", "cc", trackId); - insertWaypoint("bb", "cc", "dd", trackId); - Marker.Id titleWaypointId = insertWaypoint("aa", "bb", "cc", trackId); - insertTrack("bb", "cc", "dd"); - - SearchQuery query = new SearchQuery("a", null, null, 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)); - } - - @Test - public void testSearchWaypointDistance() { - // All results match text, but they're at difference distances from the user. - Track.Id trackId = insertTrack("", "", ""); - Marker.Id farFarAwayId = insertWaypoint("aa", 0.3, trackId); - Marker.Id nearId = insertWaypoint("ab", 0.1, trackId); - Marker.Id farId = insertWaypoint("ac", 0.2, trackId); - - SearchQuery query = new SearchQuery("a", HERE, null, 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. - Track.Id oldestId = insertTrack("aa", 3); - Track.Id recentId = insertTrack("ab", 1); - Track.Id oldId = insertTrack("ac", 2); - - SearchQuery query = new SearchQuery("a", null, null, NOW); - ArrayList results = new ArrayList<>(engine.search(query)); - - // Reverse time order. - assertTrackResults(results, recentId, oldId, oldestId); - } - - @Test - public void testSearchWaypointRecent() { - // All results match text, but they're were recorded at different times. - Track.Id trackId = insertTrack("", "", ""); - Marker.Id oldestId = insertWaypoint("aa", 2, trackId); - Marker.Id recentId = insertWaypoint("ab", 0, trackId); - Marker.Id oldId = insertWaypoint("ac", 1, trackId); - - SearchQuery query = new SearchQuery("a", null, null, NOW); - ArrayList results = new ArrayList<>(engine.search(query)); - - // Reverse time order. - assertWaypointResults(results, recentId, oldId, oldestId); - } - - @Test - public void testSearchCurrentTrack() { - // All results match text, but one of them is the current track. - Track.Id currentId = insertTrack("ab", 1); - Track.Id 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); - } - - @Test - public void testSearchCurrentTrackWaypoint() { - // All results match text, but one of them is in the current track. - Track.Id trackId1 = insertTrack("", "", ""); - Track.Id trackId2 = insertTrack("", "", ""); - - Marker.Id otherId = insertWaypoint("aa", 1, trackId2); - Marker.Id currentId = insertWaypoint("ab", 1, trackId1); - - SearchQuery query = new SearchQuery("a", null, trackId1, NOW); - ArrayList results = new ArrayList<>(engine.search(query)); - - // Waypoint in current track should be promoted. - assertWaypointResults(results, currentId, otherId); - } - - private void assertTrackResult(Track.Id trackId, ScoredResult result) { - assertNotNull("Not a track", result.track); - assertNull("Ambiguous result", result.marker); - assertEquals(trackId, result.track.getId()); - } - - private void assertTrackResults(List results, Track.Id... trackIds) { - long[] longTrackIds = new long[trackIds.length]; - for (int i = 0; i < trackIds.length; i++) { - longTrackIds[i] = trackIds[i].getId(); - } - - String errMsg = "Expected IDs=" + Arrays.toString(longTrackIds) + "; 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.marker); - assertEquals(errMsg, trackIds[i], result.track.getId()); - } - } - - private void assertWaypointResult(Marker.Id waypointId, ScoredResult result) { - assertNotNull("Not a waypoint", result.marker); - assertNull("Ambiguous result", result.track); - assertEquals(waypointId, result.marker.getId()); - } - - private void assertWaypointResults(List results, Marker.Id... waypointIds) { - long[] longWaypointIds = new long[waypointIds.length]; - for (int i = 0; i < waypointIds.length; i++) { - longWaypointIds[i] = waypointIds[i].getId(); - } - - String errMsg = "Expected IDs=" + Arrays.toString(longWaypointIds) + "; results=" + results; - assertEquals(results.size(), waypointIds.length); - for (int i = 0; i < results.size(); i++) { - ScoredResult result = results.get(i); - assertNotNull(errMsg, result.marker); - assertNull(errMsg, result.track); - assertEquals(errMsg, waypointIds[i].getId(), result.marker.getId().getId()); - } - } -} diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ec9914415..5c39189a7 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -246,16 +246,24 @@ limitations under the License. + android:label="@string/menu_markers" + android:launchMode="singleTop"> + + + + android:name="android.app.searchable" + android:resource="@xml/searchable" /> + + + - - - - - - - - diff --git a/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java b/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java index dbd25669e..468172161 100644 --- a/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/AbstractListActivity.java @@ -30,8 +30,7 @@ import de.dennisguse.opentracks.util.PreferencesUtils; /** * An abstract class for the following common tasks across - * {@link TrackListActivity}, {@link TrackRecordedActivity}, and - * {@link SearchListActivity}: + * {@link TrackListActivity}, {@link TrackRecordedActivity}, and {@link TrackRecordedActivity}: *

* - share track
* - delete tracks
@@ -48,7 +47,7 @@ public abstract class AbstractListActivity extends AbstractActivity implements C @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (requestCode == DELETE_REQUEST_CODE) { - onDeleted(); + onTrackDeleted(); } else { super.onActivityResult(requestCode, resultCode, data); } @@ -93,5 +92,5 @@ public abstract class AbstractListActivity extends AbstractActivity implements C /** * Called after {@link TrackDeleteActivity} returns its result. */ - abstract protected void onDeleted(); + abstract protected void onTrackDeleted(); } diff --git a/src/main/java/de/dennisguse/opentracks/MarkerDetailActivity.java b/src/main/java/de/dennisguse/opentracks/MarkerDetailActivity.java index 434df2601..a7010461c 100644 --- a/src/main/java/de/dennisguse/opentracks/MarkerDetailActivity.java +++ b/src/main/java/de/dennisguse/opentracks/MarkerDetailActivity.java @@ -113,7 +113,7 @@ public class MarkerDetailActivity extends AbstractActivity implements DeleteMark } @Override - public void onDeleteMarkerDone() { + public void onMarkerDeleted() { runOnUiThread(this::finish); } diff --git a/src/main/java/de/dennisguse/opentracks/MarkerListActivity.java b/src/main/java/de/dennisguse/opentracks/MarkerListActivity.java index 3efd3f81d..91f492893 100644 --- a/src/main/java/de/dennisguse/opentracks/MarkerListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/MarkerListActivity.java @@ -16,6 +16,7 @@ package de.dennisguse.opentracks; +import android.app.SearchManager; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -28,9 +29,10 @@ import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; +import androidx.appcompat.widget.SearchView; import androidx.cursoradapter.widget.ResourceCursorAdapter; import androidx.loader.app.LoaderManager; -import androidx.loader.app.LoaderManager.LoaderCallbacks; +import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; import de.dennisguse.opentracks.content.data.Marker; @@ -67,7 +69,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - // Note that the key can be null if (PreferencesUtils.isKey(MarkerListActivity.this, R.string.recording_track_id_key, key)) { recordingTrackId = PreferencesUtils.getRecordingTrackId(MarkerListActivity.this); } @@ -84,6 +85,8 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker private MarkerListBinding viewBinding; + private final MarkerLoaderCallback loaderCallbacks = new MarkerLoaderCallback(); + // Callback when an item is selected in the contextual action mode private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { @Override @@ -135,8 +138,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker int categoryIndex = cursor.getColumnIndex(MarkerColumns.CATEGORY); int descriptionIndex = cursor.getColumnIndex(MarkerColumns.DESCRIPTION); int photoUrlIndex = cursor.getColumnIndex(MarkerColumns.PHOTOURL); - int latitudeIndex = cursor.getColumnIndex(MarkerColumns.LATITUDE); - int longitudeIndex = cursor.getColumnIndex(MarkerColumns.LONGITUDE); int iconId = MarkerUtils.ICON_ID; String name = cursor.getString(nameIndex); @@ -144,9 +145,6 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker String category = cursor.getString(categoryIndex); String description = cursor.getString(descriptionIndex); String photoUrl = cursor.getString(photoUrlIndex); - //TODO also show latitude and longitude in list - double latitude = cursor.getDouble(latitudeIndex); - double longitude = cursor.getDouble(longitudeIndex); ListItemUtils.setListItem(MarkerListActivity.this, view, false, true, iconId, R.string.image_marker, name, null, null, 0, time, false, category, description, photoUrl); } @@ -154,23 +152,7 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker viewBinding.markerList.setAdapter(resourceCursorAdapter); ActivityUtils.configureListViewContextualMenu(viewBinding.markerList, contextualActionModeCallback); - LoaderManager.getInstance(this).initLoader(0, null, new LoaderCallbacks() { - @NonNull - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) { - return ContentProviderUtils.getMarkersLoader(MarkerListActivity.this, track != null ? track.getId() : null); - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - resourceCursorAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - resourceCursorAdapter.swapCursor(null); - } - }); + loadData(getIntent()); } @Override @@ -230,14 +212,14 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker /** * Handles a context item selection. * - * @param itemId the menu item id - * @param longMarkerds the marker ids + * @param itemId the menu item id + * @param longMarkerIds the marker ids * @return true if handled. */ - private boolean handleContextItem(int itemId, long... longMarkerds) { - Marker.Id[] markerIds = new Marker.Id[longMarkerds.length]; - for (int i = 0; i < longMarkerds.length; i++) { - markerIds[i] = new Marker.Id(longMarkerds[i]); + private boolean handleContextItem(int itemId, long... longMarkerIds) { + Marker.Id[] markerIds = new Marker.Id[longMarkerIds.length]; + for (int i = 0; i < longMarkerIds.length; i++) { + markerIds[i] = new Marker.Id(longMarkerIds[i]); } Intent intent; @@ -279,7 +261,92 @@ public class MarkerListActivity extends AbstractActivity implements DeleteMarker } @Override - public void onDeleteMarkerDone() { + public void onBackPressed() { + if (loaderCallbacks.getSearchQuery() != null) { + loaderCallbacks.setSearch(null); + return; + } + SearchView searchView = (SearchView) searchMenuItem.getActionView(); + if (!searchView.isIconified()) { + searchView.setIconified(true); + return; + } + super.onBackPressed(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + loadData(intent); + } + + private void loadData(Intent intent) { + String searchQuery = null; + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + searchQuery = intent.getStringExtra(SearchManager.QUERY); + } + + loaderCallbacks.setSearch(searchQuery); + } + + @Override + public void onMarkerDeleted() { // Do nothing } + + private class MarkerLoaderCallback implements LoaderManager.LoaderCallbacks { + + private String searchQuery = null; + + public String getSearchQuery() { + return searchQuery; + } + + public void setSearch(String searchQuery) { + this.searchQuery = searchQuery; + restart(); + if (searchQuery != null) { + setTitle(searchQuery); + } else { + setTitle(R.string.menu_markers); + } + } + + public void restart() { + LoaderManager.getInstance(MarkerListActivity.this).restartLoader(0, null, loaderCallbacks); + } + + @NonNull + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + final String[] PROJECTION = new String[]{MarkerColumns._ID, + MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY, + MarkerColumns.TIME, MarkerColumns.PHOTOURL}; + + if (searchQuery == null) { + if (track != null) { + return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, MarkerColumns.TRACKID + "=?", new String[]{String.valueOf(track.getId().getId())}, null); + } else { + return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null); + } + } else { + final String SEARCH_QUERY = MarkerColumns.NAME + " LIKE ? OR " + + MarkerColumns.DESCRIPTION + " LIKE ? OR " + + MarkerColumns.CATEGORY + " LIKE ?"; + final String[] selectionArgs = new String[]{searchQuery, searchQuery, searchQuery}; + return new CursorLoader(MarkerListActivity.this, MarkerColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, MarkerColumns.DEFAULT_SORT_ORDER + " DESC"); + } + } + + @Override + public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { + resourceCursorAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(@NonNull Loader loader) { + resourceCursorAdapter.swapCursor(null); + } + } } diff --git a/src/main/java/de/dennisguse/opentracks/SearchListActivity.java b/src/main/java/de/dennisguse/opentracks/SearchListActivity.java deleted file mode 100644 index 8decb4093..000000000 --- a/src/main/java/de/dennisguse/opentracks/SearchListActivity.java +++ /dev/null @@ -1,446 +0,0 @@ -/* - * 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 de.dennisguse.opentracks; - -import android.app.SearchManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.content.SharedPreferences.OnSharedPreferenceChangeListener; -import android.os.Bundle; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.widget.ArrayAdapter; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedSet; - -import de.dennisguse.opentracks.content.SearchEngine; -import de.dennisguse.opentracks.content.SearchEngine.ScoredResult; -import de.dennisguse.opentracks.content.SearchEngine.SearchQuery; -import de.dennisguse.opentracks.content.data.Marker; -import de.dennisguse.opentracks.content.data.Track; -import de.dennisguse.opentracks.content.provider.ContentProviderUtils; -import de.dennisguse.opentracks.databinding.SearchListBinding; -import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment; -import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment; -import de.dennisguse.opentracks.fragments.DeleteMarkerDialogFragment.DeleteMarkerCaller; -import de.dennisguse.opentracks.services.TrackRecordingServiceConnection; -import de.dennisguse.opentracks.stats.TrackStatistics; -import de.dennisguse.opentracks.util.ActivityUtils; -import de.dennisguse.opentracks.util.IntentUtils; -import de.dennisguse.opentracks.util.ListItemUtils; -import de.dennisguse.opentracks.util.MarkerUtils; -import de.dennisguse.opentracks.util.PreferencesUtils; -import de.dennisguse.opentracks.util.StringUtils; -import de.dennisguse.opentracks.util.TrackIconUtils; - -/** - * An activity to display a list of searchable results. - * - * @author Rodrigo Damazio - *

- * TODO: allow to refine searchable (present searchable in context menu) - */ -public class SearchListActivity extends AbstractListActivity implements DeleteMarkerCaller, ConfirmDeleteDialogFragment.ConfirmDeleteCaller { - - private static final String TAG = SearchListActivity.class.getSimpleName(); - - private static final String IS_RECORDING_FIELD = "isRecording"; - private static final String IS_PAUSED_FIELD = "isPaused"; - private static final String ICON_ID_FIELD = "icon"; - private static final String ICON_CONTENT_DESCRIPTION_ID_FIELD = "iconContentDescription"; - private static final String NAME_FIELD = "name"; - private static final String TOTAL_TIME_FIELD = "totalTime"; - private static final String TOTAL_DISTANCE_FIELD = "totalDistance"; - private static final String MARKER_COUNT_FIELD = "markerCount"; - private static final String START_TIME_FIELD = "startTime"; - private static final String CATEGORY_FIELD = "category"; - private static final String DESCRIPTION_FIELD = "description"; - private static final String PHOTO_URL_FIELD = "photoUrl"; - private static final String MARKER_LATITUDE_FIELD = "latitude"; - private static final String MARKER_LONGITUDE_FIELD = "longitude"; - private static final String TRACK_ID_FIELD = "trackId"; - private static final String MARKER_ID_FIELD = "markerId"; - - private ContentProviderUtils contentProviderUtils; - - private SharedPreferences sharedPreferences; - - private TrackRecordingServiceConnection trackRecordingServiceConnection; - - private SearchEngine searchEngine; - - private ArrayAdapter> arrayAdapter; - - private boolean metricUnits = true; - - private Track.Id recordingTrackId; - - private boolean recordingTrackPaused; - - private SearchListBinding viewBinding; - - // Callback when an item is selected in the contextual action mode - private final ContextualActionModeCallback contextualActionModeCallback = new ContextualActionModeCallback() { - @Override - public void onPrepare(Menu menu, int[] positions, long[] ids, boolean showSelectAll) { - boolean isRecording = PreferencesUtils.isRecording(recordingTrackId); - boolean isSingleSelection = positions.length == 1; - - boolean isSingleSelectionTrack; - if (isSingleSelection) { - Map item = arrayAdapter.getItem(positions[0]); - isSingleSelectionTrack = item.get(MARKER_ID_FIELD) == null; - } else { - isSingleSelectionTrack = false; - } - - // Not recording, one item, item is a track - MenuItem shareMenuItem = menu.findItem(R.id.list_context_menu_share); - if (isSingleSelectionTrack) { - shareMenuItem.setVisible(!isRecording); - } - - // One item, item is a marker - menu.findItem(R.id.list_context_menu_show_on_map).setVisible(isSingleSelection && !isSingleSelectionTrack); - // One item, can be a track or a marker - menu.findItem(R.id.list_context_menu_edit).setVisible(isSingleSelection); - // One item. If track, no restriction. - menu.findItem(R.id.list_context_menu_delete).setVisible(isSingleSelection && isSingleSelectionTrack); - // Disable select all, no action is available for multiple selection - menu.findItem(R.id.list_context_menu_select_all).setVisible(false); - } - - @Override - public boolean onClick(int itemId, int[] positions, long[] ids) { - return handleContextItem(itemId, positions); - } - }; - - private final OnSharedPreferenceChangeListener sharedPreferenceChangeListener = new OnSharedPreferenceChangeListener() { - @Override - public void onSharedPreferenceChanged(SharedPreferences preferences, String key) { - if (PreferencesUtils.isKey(SearchListActivity.this, R.string.stats_units_key, key)) { - metricUnits = PreferencesUtils.isMetricUnits(SearchListActivity.this); - } - if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_id_key, key)) { - recordingTrackId = PreferencesUtils.getRecordingTrackId(SearchListActivity.this); - } - if (PreferencesUtils.isKey(SearchListActivity.this, R.string.recording_track_paused_key, key)) { - recordingTrackPaused = PreferencesUtils.isRecordingTrackPaused(SearchListActivity.this); - } - if (key != null) { - runOnUiThread(() -> arrayAdapter.notifyDataSetChanged()); - } - } - }; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); - - recordingTrackPaused = PreferencesUtils.isRecordingTrackPausedDefault(this); - - contentProviderUtils = new ContentProviderUtils(this); - sharedPreferences = PreferencesUtils.getSharedPreferences(this); - trackRecordingServiceConnection = new TrackRecordingServiceConnection(); - searchEngine = new SearchEngine(contentProviderUtils); - - arrayAdapter = new ArrayAdapter>(this, R.layout.list_item, R.id.list_item_name) { - @NonNull - @Override - public View getView(int position, View convertView, @NonNull android.view.ViewGroup parent) { - View view = convertView; - if (convertView == null) { - view = getLayoutInflater().inflate(R.layout.list_item, parent, false); - } - - Map resultMap = getItem(position); - boolean isRecording = (boolean) resultMap.get(IS_RECORDING_FIELD); - boolean isPaused = (boolean) resultMap.get(IS_PAUSED_FIELD); - int iconId = (int) resultMap.get(ICON_ID_FIELD); - int iconContentDescriptionId = (int) resultMap.get(ICON_CONTENT_DESCRIPTION_ID_FIELD); - String name = (String) resultMap.get(NAME_FIELD); - String totalTime = (String) resultMap.get(TOTAL_TIME_FIELD); - String totalDistance = (String) resultMap.get(TOTAL_DISTANCE_FIELD); - int markerCount = (int) resultMap.get(MARKER_COUNT_FIELD); - long startTime = (long) resultMap.get(START_TIME_FIELD); - String category = (String) resultMap.get(CATEGORY_FIELD); - String description = (String) resultMap.get(DESCRIPTION_FIELD); - String photoUrl = (String) resultMap.get(PHOTO_URL_FIELD); - - ListItemUtils.setListItem(SearchListActivity.this, view, isRecording, isPaused, iconId, - iconContentDescriptionId, name, totalTime, totalDistance, markerCount, - startTime, false, category, description, photoUrl); - return view; - } - }; - // UI elements - viewBinding.searchList.setEmptyView(viewBinding.searchListEmpty); - viewBinding.searchList.setAdapter(arrayAdapter); - viewBinding.searchList.setOnItemClickListener((parent, view, position, id) -> { - Map item = arrayAdapter.getItem(position); - Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD); - Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD); - Intent intent = IntentUtils.newIntent(SearchListActivity.this, TrackRecordedActivity.class); - if (markerId != null) { - intent = intent.putExtra(TrackRecordedActivity.EXTRA_MARKER_ID, markerId); - } else { - intent = intent.putExtra(TrackRecordedActivity.EXTRA_TRACK_ID, trackId); - } - startActivity(intent); - }); - ActivityUtils.configureListViewContextualMenu(viewBinding.searchList, contextualActionModeCallback); - handleIntent(getIntent()); - } - - @Override - protected void onStart() { - super.onStart(); - sharedPreferences.registerOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - sharedPreferenceChangeListener.onSharedPreferenceChanged(null, null); - trackRecordingServiceConnection.startConnection(this); - } - - @Override - protected void onResume() { - super.onResume(); - arrayAdapter.notifyDataSetChanged(); - } - - @Override - protected void onStop() { - super.onStop(); - sharedPreferences.unregisterOnSharedPreferenceChangeListener(sharedPreferenceChangeListener); - trackRecordingServiceConnection.unbind(this); - } - - @Override - protected View getRootView() { - viewBinding = SearchListBinding.inflate(getLayoutInflater()); - return viewBinding.getRoot(); - } - - @Override - public void onNewIntent(Intent intent) { - super.onNewIntent(intent); - setIntent(intent); - handleIntent(intent); - } - - @Override - public boolean onKeyUp(int keyCode, KeyEvent event) { - if (keyCode == KeyEvent.KEYCODE_SEARCH) { - return true; - } - return super.onKeyUp(keyCode, event); - } - - /** - * Handles a context item selection. - * - * @param itemId the menu item id - * @param positions the positions of the selected rows - * @return true if handled. - */ - private boolean handleContextItem(int itemId, int[] positions) { - if (positions.length != 1) { - return false; - } - Map item = arrayAdapter.getItem(positions[0]); - Track.Id trackId = (Track.Id) item.get(TRACK_ID_FIELD); - Marker.Id markerId = (Marker.Id) item.get(MARKER_ID_FIELD); - Intent intent; - switch (itemId) { - case R.id.list_context_menu_show_on_map: - IntentUtils.showCoordinateOnMap(this, (double) item.get(MARKER_LATITUDE_FIELD), (double) item.get(MARKER_LONGITUDE_FIELD), item.get(NAME_FIELD) + ""); - return true; - case R.id.list_context_menu_share: - intent = IntentUtils.newShareFileIntent(this, trackId); - intent = Intent.createChooser(intent, null); - startActivity(intent); - return true; - case R.id.list_context_menu_edit: - if (markerId != null) { - intent = IntentUtils.newIntent(this, MarkerEditActivity.class) - .putExtra(MarkerEditActivity.EXTRA_MARKER_ID, markerId); - } else { - intent = IntentUtils.newIntent(this, TrackEditActivity.class) - .putExtra(TrackEditActivity.EXTRA_TRACK_ID, trackId); - } - startActivity(intent); - - // Close the searchable result since its content can change after edit. - finish(); - return true; - case R.id.list_context_menu_delete: - if (markerId != null) { - DeleteMarkerDialogFragment.showDialog(getSupportFragmentManager(), markerId); - } else { - deleteTracks(trackId); - } - return true; - default: - return false; - } - } - - /** - * Handles the intent. - * - * @param intent the intent - */ - private void handleIntent(Intent intent) { - if (!Intent.ACTION_SEARCH.equals(intent.getAction())) { - Log.e(TAG, "Invalid intent action: " + intent); - finish(); - return; - } - - final String textQuery = intent.getStringExtra(SearchManager.QUERY); - setTitle(textQuery); - - doSearch(textQuery); - } - - /** - * Do the searchable. - * - * @param textQuery the query - */ - private void doSearch(String textQuery) { - SearchQuery query = new SearchQuery(textQuery, null, null, System.currentTimeMillis()); - SortedSet scoredResults = searchEngine.search(query); - final List> displayResults = prepareResultsforDisplay(scoredResults); - - // Use the UI thread to display the results - runOnUiThread(() -> { - arrayAdapter.clear(); - arrayAdapter.addAll(displayResults); - }); - } - - /** - * Prepares the result for display. - * - * @param scoredResults a list of score results - * @return a list of result maps - */ - private List> prepareResultsforDisplay(Collection scoredResults) { - //TODO Replace use of map, but rather provide Track or Marker directly. - ArrayList> output = new ArrayList<>(scoredResults.size()); - for (ScoredResult result : scoredResults) { - if (result.track != null) { - output.add(prepareTrackForDisplay(result.track)); - } else { - output.add(prepareMarkerForDisplay(result.marker)); - } - } - return output; - } - - private Map prepareMarkerForDisplay(Marker marker) { - Map resultMap = new HashMap<>(); - - //TODO: It may be more appropriate to obtain the track name as a join in the retrieval phase of the searchable. - String trackName = null; - Track.Id trackId = marker.getTrackId(); - if (!trackId.isValid()) { - Track track = contentProviderUtils.getTrack(trackId); - if (track != null) { - trackName = track.getName(); - } - } - - resultMap.put(IS_RECORDING_FIELD, false); - resultMap.put(IS_PAUSED_FIELD, true); - resultMap.put(ICON_ID_FIELD, MarkerUtils.ICON_ID); - resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_marker); - resultMap.put(NAME_FIELD, marker.getName()); - // Display the marker's track name in the total time field - resultMap.put(TOTAL_TIME_FIELD, trackName == null ? null : getString(R.string.search_list_marker_track_location, trackName)); - resultMap.put(TOTAL_DISTANCE_FIELD, null); - resultMap.put(MARKER_COUNT_FIELD, 0); - resultMap.put(START_TIME_FIELD, marker.getLocation().getTime()); - resultMap.put(CATEGORY_FIELD, marker.getCategory()); - resultMap.put(DESCRIPTION_FIELD, marker.getDescription()); - resultMap.put(PHOTO_URL_FIELD, marker.getPhotoUrl()); - resultMap.put(TRACK_ID_FIELD, marker.getTrackId()); - resultMap.put(MARKER_ID_FIELD, marker.getId()); - - resultMap.put(MARKER_LATITUDE_FIELD, marker.getLocation().getLatitude()); - resultMap.put(MARKER_LONGITUDE_FIELD, marker.getLocation().getLongitude()); - - return resultMap; - } - - /** - * Prepares a track for display by filling in a result map. - * - * @param track the track - * @return the result map - */ - private Map prepareTrackForDisplay(Track track) { - Map resultMap = new HashMap<>(); - - TrackStatistics trackStatistics = track.getTrackStatistics(); - String icon = track.getIcon(); - String category = icon != null && !icon.equals("") ? null : track.getCategory(); - - resultMap.put(IS_RECORDING_FIELD, track.getId().equals(recordingTrackId)); - resultMap.put(IS_PAUSED_FIELD, recordingTrackPaused); - resultMap.put(ICON_ID_FIELD, TrackIconUtils.getIconDrawable(icon)); - resultMap.put(ICON_CONTENT_DESCRIPTION_ID_FIELD, R.string.image_track); - resultMap.put(NAME_FIELD, track.getName()); - resultMap.put(TOTAL_TIME_FIELD, StringUtils.formatElapsedTime(trackStatistics.getTotalTime())); - resultMap.put(TOTAL_DISTANCE_FIELD, StringUtils.formatDistance(this, trackStatistics.getTotalDistance(), metricUnits)); - resultMap.put(MARKER_COUNT_FIELD, contentProviderUtils.getMarkerCount(track.getId())); - resultMap.put(START_TIME_FIELD, trackStatistics.getStartTime_ms()); - resultMap.put(CATEGORY_FIELD, category); - resultMap.put(DESCRIPTION_FIELD, track.getDescription()); - resultMap.put(PHOTO_URL_FIELD, null); - resultMap.put(TRACK_ID_FIELD, track.getId()); - resultMap.put(MARKER_ID_FIELD, null); - - return resultMap; - } - - @Override - public void onDeleteMarkerDone() { - runOnUiThread(() -> handleIntent(getIntent())); - } - - @Override - protected TrackRecordingServiceConnection getTrackRecordingServiceConnection() { - return trackRecordingServiceConnection; - } - - @Override - protected void onDeleted() { - runOnUiThread(() -> handleIntent(getIntent())); - } -} diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java index 4259bbbda..bedb19c9c 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java @@ -41,7 +41,6 @@ import androidx.appcompat.widget.SearchView; import androidx.core.app.ActivityCompat; import androidx.cursoradapter.widget.ResourceCursorAdapter; import androidx.loader.app.LoaderManager; -import androidx.loader.app.LoaderManager.LoaderCallbacks; import androidx.loader.content.CursorLoader; import androidx.loader.content.Loader; @@ -60,7 +59,6 @@ import de.dennisguse.opentracks.util.ListItemUtils; import de.dennisguse.opentracks.util.PreferencesUtils; import de.dennisguse.opentracks.util.StringUtils; import de.dennisguse.opentracks.util.TrackIconUtils; -import de.dennisguse.opentracks.util.TrackUtils; /** * An activity displaying a list of tracks. @@ -80,26 +78,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe private TrackListBinding viewBinding; - private final LoaderCallbacks loaderCallbacks = new LoaderCallbacks() { - @Override - public Loader onCreateLoader(int arg0, Bundle arg1) { - String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME, - TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, - TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, "markerCount"}; - - return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, TrackUtils.TRACK_SORT_ORDER); - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { - resourceCursorAdapter.swapCursor(cursor); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - resourceCursorAdapter.swapCursor(null); - } - }; + private final TrackLoaderCallBack loaderCallbacks = new TrackLoaderCallBack(); // Preferences private boolean metricUnits = true; @@ -117,8 +96,8 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe } @Override - public boolean onClick(int itemId, int[] positions, long[] ids) { - return handleContextItem(itemId, ids); + public boolean onClick(int itemId, int[] positions, long[] trackIds) { + return handleContextItem(itemId, trackIds); } }; @@ -142,7 +121,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe if (key != null) { runOnUiThread(() -> { TrackListActivity.this.invalidateOptionsMenu(); - LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks); + loaderCallbacks.restart(); boolean isRecording = PreferencesUtils.isRecording(recordingTrackId); trackController.update(isRecording, recordingTrackPaused); }); @@ -223,6 +202,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe setTheme(R.style.ThemeCustom); super.onCreate(savedInstanceState); + setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); gpsStatusValue = GpsStatusValue.GPS_NONE; @@ -233,7 +213,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe trackRecordingServiceConnection = new TrackRecordingServiceConnection(bindChangedCallback); trackController = new TrackController(this, viewBinding.trackControllerContainer, trackRecordingServiceConnection, true, recordListener, stopListener); - setDefaultKeyMode(DEFAULT_KEYS_SEARCH_LOCAL); // Show trackController when search dialog is dismissed SearchManager searchManager = (SearchManager) getSystemService(SEARCH_SERVICE); @@ -287,10 +266,9 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe } }; viewBinding.trackList.setAdapter(resourceCursorAdapter); - ActivityUtils.configureListViewContextualMenu(viewBinding.trackList, contextualActionModeCallback); - LoaderManager.getInstance(this).initLoader(0, null, loaderCallbacks); + loadData(getIntent()); requestGPSPermissions(); } @@ -304,6 +282,14 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe trackRecordingServiceConnection.startConnection(this); } + @Override + protected void onPause() { + super.onPause(); + + // Update UI + trackController.onPause(); + } + @Override protected void onResume() { super.onResume(); @@ -314,14 +300,6 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe trackController.onResume(PreferencesUtils.isRecording(recordingTrackId), recordingTrackPaused); } - @Override - protected void onPause() { - super.onPause(); - - // Update UI - trackController.onPause(); - } - @Override protected void onStop() { super.onStop(); @@ -420,6 +398,36 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe return super.onKeyUp(keyCode, event); } + @Override + public void onBackPressed() { + if (loaderCallbacks.getSearchQuery() != null) { + loaderCallbacks.setSearch(null); + return; + } + SearchView searchView = (SearchView) searchMenuItem.getActionView(); + if (!searchView.isIconified()) { + searchView.setIconified(true); + return; + } + super.onBackPressed(); + } + + @Override + public void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + loadData(intent); + } + + private void loadData(Intent intent) { + String searchQuery = null; + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + searchQuery = intent.getStringExtra(SearchManager.QUERY); + } + + loaderCallbacks.setSearch(searchQuery); + } + @Override public boolean onSearchRequested() { // Hide trackController when search dialog is shown @@ -433,7 +441,7 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe } @Override - protected void onDeleted() { + protected void onTrackDeleted() { // Do nothing } @@ -516,4 +524,57 @@ public class TrackListActivity extends AbstractListActivity implements ConfirmDe } return false; } + + private class TrackLoaderCallBack implements LoaderManager.LoaderCallbacks { + + private String searchQuery = null; + + public String getSearchQuery() { + return searchQuery; + } + + public void setSearch(String searchQuery) { + this.searchQuery = searchQuery; + restart(); + if (searchQuery != null) { + setTitle(searchQuery); + } else { + setTitle(R.string.app_name); + } + } + + public void restart() { + LoaderManager.getInstance(TrackListActivity.this).restartLoader(0, null, loaderCallbacks); + } + + @NonNull + @Override + public Loader onCreateLoader(int arg0, Bundle arg1) { + final String[] PROJECTION = new String[]{TracksColumns._ID, TracksColumns.NAME, + TracksColumns.DESCRIPTION, TracksColumns.CATEGORY, TracksColumns.STARTTIME, + TracksColumns.TOTALDISTANCE, TracksColumns.TOTALTIME, TracksColumns.ICON, TracksColumns.MARKER_COUNT}; + + final String sortOrder = TracksColumns.STARTTIME + " DESC"; + + if (searchQuery == null) { + return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, null, null, sortOrder); + } else { + final String SEARCH_QUERY = TracksColumns.NAME + " LIKE ? OR " + + TracksColumns.DESCRIPTION + " LIKE ? OR " + + TracksColumns.CATEGORY + " LIKE ?"; + final String[] selectionArgs = new String[]{searchQuery, searchQuery, searchQuery}; + return new CursorLoader(TrackListActivity.this, TracksColumns.CONTENT_URI, PROJECTION, SEARCH_QUERY, selectionArgs, sortOrder); + } + } + + @Override + public void onLoadFinished(@NonNull Loader loader, Cursor cursor) { + resourceCursorAdapter.swapCursor(cursor); + } + + @Override + public void onLoaderReset(@NonNull Loader loader) { + resourceCursorAdapter.swapCursor(null); + } + } } diff --git a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java index 5f11faffc..b124efaf7 100644 --- a/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java +++ b/src/main/java/de/dennisguse/opentracks/TrackRecordedActivity.java @@ -188,7 +188,7 @@ public class TrackRecordedActivity extends AbstractListActivity implements Confi } @Override - protected void onDeleted() { + protected void onTrackDeleted() { runOnUiThread(this::finish); } diff --git a/src/main/java/de/dennisguse/opentracks/content/SearchEngine.java b/src/main/java/de/dennisguse/opentracks/content/SearchEngine.java deleted file mode 100644 index d0169e5dc..000000000 --- a/src/main/java/de/dennisguse/opentracks/content/SearchEngine.java +++ /dev/null @@ -1,425 +0,0 @@ -/* - * 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 de.dennisguse.opentracks.content; - -import android.database.Cursor; -import android.location.Location; - -import androidx.annotation.NonNull; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.SortedSet; -import java.util.TreeSet; - -import de.dennisguse.opentracks.content.data.Marker; -import de.dennisguse.opentracks.content.data.MarkerColumns; -import de.dennisguse.opentracks.content.data.Track; -import de.dennisguse.opentracks.content.data.TracksColumns; -import de.dennisguse.opentracks.content.provider.ContentProviderUtils; -import de.dennisguse.opentracks.stats.TrackStatistics; -import de.dennisguse.opentracks.util.LocationUtils; -import de.dennisguse.opentracks.util.UnitConversions; - -/** - * Engine for searching for tracks and markers 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 markers by name. - */ - private static final String MARKER_SELECTION_QUERY = - MarkerColumns.NAME + " LIKE ? OR " + - MarkerColumns.DESCRIPTION + " LIKE ? OR " + - MarkerColumns.CATEGORY + " LIKE ?"; - - /** - * Order of track results. - */ - private static final String TRACK_SELECTION_ORDER = TracksColumns._ID + " DESC LIMIT 1000"; - - /** - * Order of marker results. - */ - private static final String MARKER_SELECTION_ORDER = MarkerColumns._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 marker result if it's in the currently-selected track. - */ - private static final double CURRENT_TRACK_MARKER_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 markers which will be retrieved and scored. - */ - private static final int MAX_SCORED_MARKERS = 100; - - /** - * Oldest timestamp for which we rank based on time (2000-01-01 00:00:00.000) - */ - public static final long OLDEST_ALLOWED_TIMESTAMP = 946692000000L; - - /** - * Comparator for scored results. - */ - private static final Comparator SCORED_RESULT_COMPARATOR = (r1, r2) -> { - // Score ordering. - int scoreDiff = Double.compare(r2.score, r1.score); - if (scoreDiff != 0) { - return scoreDiff; - } - - // Make tracks come before markers. - if (r1.marker != null && r2.track != null) { - return 1; - } else if (r1.track != null && r2.marker != null) { - return -1; - } - - // Finally, use arbitrary ordering, by ID. - long id1 = r1.track != null ? r1.track.getId().getId() : r1.marker.getId().getId(); - long id2 = r2.track != null ? r2.track.getId().getId() : r2.marker.getId().getId(); - long idDiff = id2 - id1; - return Long.signum(idDiff); - }; - - private final ContentProviderUtils providerUtils; - - public SearchEngine(ContentProviderUtils providerUtils) { - this.providerUtils = providerUtils; - } - - /** - * Squashes a number by calculating 1 / log (1 + x). - */ - //TODO Why is this done? - private static double squash(double x) { - return 1.0 / Math.log1p(x); - } - - /** - * Executes a searchable 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) { - TreeSet scoredResults = new TreeSet<>(SCORED_RESULT_COMPARATOR); - - scoreTrackResults(retrieveTracks(query), query, scoredResults); - scoreMarkersResults(retrieveMarkers(query), query, scoredResults); - - return scoredResults; - } - - /** - * Retrieves tracks matching the given query from the database. - * - * @param query the query to retrieve for - */ - private List retrieveTracks(SearchQuery query) { - ArrayList tracks = new ArrayList<>(); - - String queryLikeSelection = "%" + query.textQuery + "%"; - String[] trackSelectionArgs = new String[]{ - queryLikeSelection, - queryLikeSelection, - queryLikeSelection - }; - - try (Cursor cursor = providerUtils.getTrackCursor(TRACK_SELECTION_QUERY, trackSelectionArgs, TRACK_SELECTION_ORDER)) { - if (cursor != null) { - tracks.ensureCapacity(cursor.getCount()); - while (cursor.moveToNext()) { - tracks.add(providerUtils.createTrack(cursor)); - } - } - } - - return tracks; - } - - /** - * Retrieves markers matching the given query from the database. - * - * @param query the query to retrieve for - */ - private List retrieveMarkers(SearchQuery query) { - ArrayList markers = new ArrayList<>(); - - String queryLikeSelection2 = "%" + query.textQuery + "%"; - String[] markersSelectionArgs = new String[]{ - queryLikeSelection2, - queryLikeSelection2, - queryLikeSelection2 - }; - - try (Cursor cursor = providerUtils.getMarkerCursor(MARKER_SELECTION_QUERY, markersSelectionArgs, MARKER_SELECTION_ORDER, MAX_SCORED_MARKERS)) { - if (cursor != null) { - markers.ensureCapacity(cursor.getCount()); - while (cursor.moveToNext()) { - Marker marker = providerUtils.createMarker(cursor); - if (LocationUtils.isValidLocation(marker.getLocation())) { - markers.add(marker); - } - } - } - } - - return markers; - } - - /** - * 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()); - - TrackStatistics statistics = track.getTrackStatistics(); - - long meanTimestamp = (statistics.getStartTime_ms() + statistics.getStopTime_ms()) / 2L; - score *= getTimeBoost(query, meanTimestamp); - - // Score the currently-selected track lower (user is already there, wouldn't be searching for it). - if (track.getId().equals(query.currentTrackId)) { - score *= CURRENT_TRACK_DEMOTION; - } - - return score; - } - - /** - * Scores a collection of marker results. - * - * @param markers the results to score - * @param query the query to score for - * @param output the collection to fill with scored results - */ - private void scoreMarkersResults(Collection markers, SearchQuery query, Collection output) { - for (Marker marker : markers) { - // Calculate the score. - double score = scoreMarkerResult(query, marker); - - // Add to the output. - output.add(new ScoredResult(marker, score)); - } - } - - /** - * Scores a single marker result. - * - * @param query the query to score for - * @param marker the results to score - * @return the score for the marker - */ - private double scoreMarkerResult(SearchQuery query, Marker marker) { - double score = 1.0; - - Location location = marker.getLocation(); - score *= getTitleBoost(query, marker.getName(), marker.getDescription(), marker.getCategory()); - score *= getDistanceBoost(query, location.getLatitude(), location.getLongitude()); - score *= getTimeBoost(query, location.getTime()); - - // Score markers in the currently-selected track higher (searching inside the current track). - if (marker.getTrackId() != null && marker.getTrackId().equals(query.currentTrackId)) { - score *= CURRENT_TRACK_MARKER_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 marker - * @param description the description of the track or marker - * @param category the category of the track or marker - * @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(Locale.getDefault()).contains(query.textQuery)) { - boost *= TRACK_NAME_PROMOTION; - } - if (description.toLowerCase(Locale.getDefault()).contains(query.textQuery)) { - boost *= TRACK_DESCRIPTION_PROMOTION; - } - if (category.toLowerCase(Locale.getDefault()).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 = (long) ((query.currentTimestamp - timestamp) * UnitConversions.MS_TO_S * UnitConversions.S_TO_MIN * UnitConversions.MIN_TO_HR); - 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] * UnitConversions.M_TO_KM; - - 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; - } - } - - /** - * Description of a searchable query, along with all contextual data needed to execute it. - */ - public static class SearchQuery { - final String textQuery; - final Location currentLocation; - final Track.Id currentTrackId; - final long currentTimestamp; - - public SearchQuery(String textQuery, Location currentLocation, Track.Id currentTrackId, long currentTimestamp) { - this.textQuery = textQuery.toLowerCase(Locale.getDefault()); - this.currentLocation = currentLocation; - this.currentTrackId = currentTrackId; - this.currentTimestamp = currentTimestamp; - } - } - - /** - * Description of a searchable result which has been retrieved and scored. - */ - public static class ScoredResult { - public final Track track; - public final Marker marker; - final double score; - - ScoredResult(Track track, double score) { - this.track = track; - this.marker = null; - this.score = score; - } - - ScoredResult(Marker marker, double score) { - this.track = null; - this.marker = marker; - this.score = score; - } - - @NonNull - @Override - public String toString() { - return "ScoredResult [" - + (track != null ? ("trackId=" + track.getId().getId() + ", ") : "") - + (marker != null ? ("wptId=" + marker.getId().getId() + ", ") : "") - + "score=" + score + "]"; - } - } -} diff --git a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java index 25313dc8c..600f4a589 100644 --- a/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java +++ b/src/main/java/de/dennisguse/opentracks/content/provider/ContentProviderUtils.java @@ -28,8 +28,6 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import androidx.loader.content.CursorLoader; -import androidx.loader.content.Loader; import java.io.File; import java.util.ArrayList; @@ -415,8 +413,7 @@ public class ContentProviderUtils { if (!markerId.isValid()) { return null; } - try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", - new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) { + try (Cursor cursor = getMarkerCursor(null, MarkerColumns._ID + "=?", new String[]{Long.toString(markerId.getId())}, MarkerColumns._ID, 1)) { if (cursor != null && cursor.moveToFirst()) { return createMarker(cursor); } @@ -424,18 +421,6 @@ public class ContentProviderUtils { return null; } - /** - * The caller owns the returned cursor and is responsible for closing it. - * - * @param selection the selection. Can be null - * @param selectionArgs the selection arguments. Can be null - * @param sortOrder the sort order. Can be null - * @param maxCount the maximum number of markers to return. -1 for no limit - */ - public Cursor getMarkerCursor(String selection, String[] selectionArgs, String sortOrder, int maxCount) { - return getMarkerCursor(null, selection, selectionArgs, sortOrder, maxCount); - } - /** * The caller owns the returned cursor and is responsible for closing it. * @@ -474,21 +459,6 @@ public class ContentProviderUtils { return markers; } - public static Loader getMarkersLoader(Context context, @Nullable Track.Id trackId) { - final String[] PROJECTION = new String[]{MarkerColumns._ID, - MarkerColumns.NAME, MarkerColumns.DESCRIPTION, MarkerColumns.CATEGORY, - MarkerColumns.TIME, MarkerColumns.PHOTOURL, - MarkerColumns.LATITUDE, MarkerColumns.LONGITUDE}; - - if (trackId != null) { - return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION, - MarkerColumns.TRACKID + "=?", - new String[]{String.valueOf(trackId.getId())}, null); - } else { - return new CursorLoader(context, MarkerColumns.CONTENT_URI, PROJECTION, null, null, null); - } - } - @Deprecated //TODO TracksColumns.MARKER_COUNT while querying for tracks public int getMarkerCount(Track.Id trackId) { if (!trackId.isValid()) { diff --git a/src/main/java/de/dennisguse/opentracks/fragments/DeleteMarkerDialogFragment.java b/src/main/java/de/dennisguse/opentracks/fragments/DeleteMarkerDialogFragment.java index 9198095c1..b4153f9d2 100644 --- a/src/main/java/de/dennisguse/opentracks/fragments/DeleteMarkerDialogFragment.java +++ b/src/main/java/de/dennisguse/opentracks/fragments/DeleteMarkerDialogFragment.java @@ -82,7 +82,7 @@ public class DeleteMarkerDialogFragment extends DialogFragment { for (Marker.Id markerId : markerIds) { contentProviderUtils.deleteMarker(getContext(), markerId); } - caller.onDeleteMarkerDone(); + caller.onMarkerDeleted(); }).start()); } @@ -96,6 +96,6 @@ public class DeleteMarkerDialogFragment extends DialogFragment { /** * Called when delete marker is done. */ - void onDeleteMarkerDone(); + void onMarkerDeleted(); } } \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java index 1d34982a0..37510133c 100644 --- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java +++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/ExportActivity.java @@ -24,7 +24,6 @@ import android.view.View; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; import androidx.documentfile.provider.DocumentFile; import androidx.fragment.app.FragmentActivity; diff --git a/src/main/java/de/dennisguse/opentracks/util/ActivityUtils.java b/src/main/java/de/dennisguse/opentracks/util/ActivityUtils.java index 9de9331ed..2455e8827 100644 --- a/src/main/java/de/dennisguse/opentracks/util/ActivityUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/ActivityUtils.java @@ -2,7 +2,6 @@ package de.dennisguse.opentracks.util; import android.app.Activity; import android.app.SearchManager; -import android.content.ComponentName; import android.content.Context; import android.os.Build; import android.os.VibrationEffect; @@ -23,7 +22,6 @@ import java.util.ArrayList; import de.dennisguse.opentracks.ContextualActionModeCallback; import de.dennisguse.opentracks.R; -import de.dennisguse.opentracks.SearchListActivity; import de.dennisguse.opentracks.TrackController; import static android.content.Context.VIBRATOR_SERVICE; @@ -93,8 +91,8 @@ public class ActivityUtils { final SearchView searchView = (SearchView) menuItem.getActionView(); SearchManager searchManager = (SearchManager) activity.getSystemService(Context.SEARCH_SERVICE); if (searchManager != null) { - //NOTE: for some reason activity.getComponentName() did not trigger the SearchListActivity - searchView.setSearchableInfo(searchManager.getSearchableInfo(new ComponentName(activity, SearchListActivity.class))); + //NOTE: Use searchManager.getSearchableInfo(new ComponentName(activity, SearchActivity.class)) if another activity should handle the search + searchView.setSearchableInfo(searchManager.getSearchableInfo(activity.getComponentName())); } else { Log.w(TAG, "Could not retrieve SearchManager."); } diff --git a/src/main/java/de/dennisguse/opentracks/util/TrackUtils.java b/src/main/java/de/dennisguse/opentracks/util/TrackUtils.java index 79d354bc2..a6a5d6404 100644 --- a/src/main/java/de/dennisguse/opentracks/util/TrackUtils.java +++ b/src/main/java/de/dennisguse/opentracks/util/TrackUtils.java @@ -19,7 +19,6 @@ package de.dennisguse.opentracks.util; import android.content.Context; import de.dennisguse.opentracks.content.data.Track; -import de.dennisguse.opentracks.content.data.TracksColumns; import de.dennisguse.opentracks.content.provider.ContentProviderUtils; /** @@ -29,8 +28,6 @@ import de.dennisguse.opentracks.content.provider.ContentProviderUtils; */ public class TrackUtils { - public static final String TRACK_SORT_ORDER = TracksColumns.STARTTIME + " DESC"; - private TrackUtils() { } diff --git a/src/main/res/layout/search_list.xml b/src/main/res/layout/search_list.xml deleted file mode 100644 index 3e09e1c5f..000000000 --- a/src/main/res/layout/search_list.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/main/res/xml/searchable.xml b/src/main/res/xml/searchable.xml index 9317b322b..d3eafaf47 100644 --- a/src/main/res/xml/searchable.xml +++ b/src/main/res/xml/searchable.xml @@ -2,6 +2,4 @@ + android:label="@string/app_name" />