mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-09-30 16:52:21 +02:00
AggregatedStatisticsActivity: added filter option (by category, start and/or end date).
Fixes #1039.
This commit is contained in:
committed by
Román Martínez
parent
4d333bbafa
commit
b4ce99440a
Generated
+8
-1
@@ -1,9 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="src/main/res/layout/aggregated_stats.xml" value="0.17147922998986828" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
</project>
|
||||
@@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
viewBox="0 0 24 24"
|
||||
id="vector"
|
||||
version="1.1"
|
||||
sodipodi:docname="ic_clear_filter.svg"
|
||||
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
|
||||
<metadata
|
||||
id="metadata8">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<defs
|
||||
id="defs6" />
|
||||
<sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1"
|
||||
objecttolerance="10"
|
||||
gridtolerance="10"
|
||||
guidetolerance="10"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1011"
|
||||
id="namedview4"
|
||||
showgrid="false"
|
||||
inkscape:zoom="16.541667"
|
||||
inkscape:cx="12.392292"
|
||||
inkscape:cy="24.710689"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="vector" />
|
||||
<path
|
||||
id="path"
|
||||
style="fill:#707070;fill-opacity:1"
|
||||
d="M 5.0390625 4 C 4.2090625 4 3.74 4.949375 4.25 5.609375 C 6.0862696 7.9637998 8.9006606 11.587089 9.4765625 12.328125 L 17.804688 4 L 5.0390625 4 z M 14 13.958984 L 10 17.958984 L 10 19 C 10 19.55 10.45 20 11 20 L 13 20 C 13.55 20 14 19.55 14 19 L 14 13.958984 z " />
|
||||
<rect
|
||||
style="fill:#707070;fill-opacity:1;stroke:none;stroke-width:1.118;stroke-linecap:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect833"
|
||||
width="21.37405"
|
||||
height="1.7606405"
|
||||
x="-10.108474"
|
||||
y="16.886091"
|
||||
ry="0.88032025"
|
||||
transform="rotate(-45)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,81 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import static androidx.test.espresso.Espresso.onView;
|
||||
import static androidx.test.espresso.action.ViewActions.click;
|
||||
import static androidx.test.espresso.assertion.ViewAssertions.matches;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withId;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withParent;
|
||||
import static androidx.test.espresso.matcher.ViewMatchers.withText;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.test.core.app.ApplicationProvider;
|
||||
import androidx.test.espresso.ViewInteraction;
|
||||
import androidx.test.ext.junit.rules.ActivityScenarioRule;
|
||||
import androidx.test.filters.LargeTest;
|
||||
import androidx.test.rule.GrantPermissionRule;
|
||||
import androidx.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.hamcrest.core.IsInstanceOf;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.TestDataUtil;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackPoint;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
@LargeTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class EspressoAggregatedFilterTest {
|
||||
|
||||
private final String CATEGORY = "category";
|
||||
|
||||
@Rule
|
||||
public ActivityScenarioRule<TrackListActivity> mActivityTestRule = new ActivityScenarioRule<>(TrackListActivity.class);
|
||||
|
||||
@Rule
|
||||
public GrantPermissionRule mGrantPermissionRule = GrantPermissionRule.grant(android.Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
Pair<Track, List<TrackPoint>> pair = TestDataUtil.createTrack(new Track.Id(System.currentTimeMillis()), 20);
|
||||
pair.first.setCategory(CATEGORY);
|
||||
TestDataUtil.insertTrackWithLocations(new ContentProviderUtils(ApplicationProvider.getApplicationContext()), pair.first, pair.second);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void espressoAggregatedFilterTest() {
|
||||
// open AggregatedStatisticsActivity through toolbar's menu item
|
||||
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.track_list_aggregated_stats))));
|
||||
onView(withId(R.id.track_list_aggregated_stats)).perform(click());
|
||||
|
||||
// open FilterDialogFragment through toolbar's menu item
|
||||
onView(withId(R.id.toolbar)).check(matches(isDisplayed()));
|
||||
onView(withId(R.id.toolbar)).check(matches(hasDescendant(withId(R.id.aggregated_statistics_filter))));
|
||||
onView(withId(R.id.aggregated_statistics_filter)).perform(click());
|
||||
|
||||
// check there's a checkbox with CATEGORY text
|
||||
ViewInteraction checkBox = onView(
|
||||
allOf(withId(R.id.filter_dialog_check_button), withText(CATEGORY),
|
||||
withParent(allOf(withId(R.id.filter_items),
|
||||
withParent(IsInstanceOf.<View>instanceOf(android.view.ViewGroup.class)))),
|
||||
isDisplayed()));
|
||||
checkBox.check(matches(isDisplayed()));
|
||||
|
||||
// check there's an edit text for "from date"
|
||||
onView(withId(R.id.filter_date_edit_text_from)).check(matches(isDisplayed()));
|
||||
|
||||
// check there's an edit text for "to date"
|
||||
onView(withId(R.id.filter_date_edit_text_to)).check(matches(isDisplayed()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import de.dennisguse.opentracks.content.provider.SelectionData;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class TrackSelectionTest extends TestCase {
|
||||
@Test
|
||||
public void testFilterBuildSelection_empty() {
|
||||
// given
|
||||
TrackSelection filter = new TrackSelection();
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertNull(selection.getSelection());
|
||||
assertNull(selection.getSelectionArgs());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_onlyOneTrackId() {
|
||||
// given
|
||||
Track.Id trackId = new Track.Id(1);
|
||||
TrackSelection filter = new TrackSelection().addTrackId(trackId);
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "_id IN (?)");
|
||||
assertEquals(selection.getSelectionArgs().length, 1);
|
||||
assertEquals(selection.getSelectionArgs()[0], "1");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_severalTracksId() {
|
||||
// given
|
||||
Track.Id trackId1 = new Track.Id(1);
|
||||
Track.Id trackId2 = new Track.Id(2);
|
||||
Track.Id trackId3 = new Track.Id(3);
|
||||
TrackSelection filter = new TrackSelection()
|
||||
.addTrackId(trackId1)
|
||||
.addTrackId(trackId2)
|
||||
.addTrackId(trackId3);
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "_id IN (?,?,?)");
|
||||
assertEquals(selection.getSelectionArgs().length, 3);
|
||||
assertEquals(selection.getSelectionArgs()[0], "1");
|
||||
assertEquals(selection.getSelectionArgs()[1], "2");
|
||||
assertEquals(selection.getSelectionArgs()[2], "3");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_onlyOneCategory() {
|
||||
// given
|
||||
TrackSelection filter = new TrackSelection().addCategory("running");
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "category IN (?)");
|
||||
assertEquals(selection.getSelectionArgs().length, 1);
|
||||
assertEquals(selection.getSelectionArgs()[0], "running");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_severalCategories() {
|
||||
// given
|
||||
TrackSelection filter = new TrackSelection()
|
||||
.addCategory("running")
|
||||
.addCategory("road biking")
|
||||
.addCategory("mountain biking")
|
||||
.addCategory("trail walking");
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "category IN (?,?,?,?)");
|
||||
assertEquals(selection.getSelectionArgs().length, 4);
|
||||
assertEquals(selection.getSelectionArgs()[0], "running");
|
||||
assertEquals(selection.getSelectionArgs()[1], "road biking");
|
||||
assertEquals(selection.getSelectionArgs()[2], "mountain biking");
|
||||
assertEquals(selection.getSelectionArgs()[3], "trail walking");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_onlyDateRange() {
|
||||
// given
|
||||
Instant instant = Instant.now();
|
||||
long oneDay = 24 * 60 * 60 * 1000;
|
||||
TrackSelection filterWrong1 = new TrackSelection().addDateRange(instant, null);
|
||||
TrackSelection filterWrong2 = new TrackSelection().addDateRange(null, instant);
|
||||
TrackSelection filterOk = new TrackSelection().addDateRange(instant, instant.plusMillis(oneDay));
|
||||
|
||||
// when
|
||||
SelectionData selectionWrong1 = filterWrong1.buildSelection();
|
||||
SelectionData selectionWrong2 = filterWrong2.buildSelection();
|
||||
SelectionData selectionOk = filterOk.buildSelection();
|
||||
|
||||
// Then
|
||||
assertNull(selectionWrong1.getSelection());
|
||||
assertNull(selectionWrong1.getSelectionArgs());
|
||||
|
||||
assertNull(selectionWrong2.getSelection());
|
||||
assertNull(selectionWrong2.getSelectionArgs());
|
||||
|
||||
assertEquals(selectionOk.getSelection(), "starttime BETWEEN ? AND ?");
|
||||
assertEquals(selectionOk.getSelectionArgs().length, 2);
|
||||
assertEquals(selectionOk.getSelectionArgs()[0], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selectionOk.getSelectionArgs()[1], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_tracksId_and_categories() {
|
||||
// given
|
||||
Track.Id trackId1 = new Track.Id(1);
|
||||
Track.Id trackId2 = new Track.Id(2);
|
||||
Track.Id trackId3 = new Track.Id(3);
|
||||
TrackSelection filter = new TrackSelection()
|
||||
.addTrackId(trackId1)
|
||||
.addTrackId(trackId2)
|
||||
.addTrackId(trackId3)
|
||||
.addCategory("running")
|
||||
.addCategory("road biking");
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "_id IN (?,?,?) AND category IN (?,?)");
|
||||
assertEquals(selection.getSelectionArgs().length, 5);
|
||||
assertEquals(selection.getSelectionArgs()[0], "1");
|
||||
assertEquals(selection.getSelectionArgs()[1], "2");
|
||||
assertEquals(selection.getSelectionArgs()[2], "3");
|
||||
assertEquals(selection.getSelectionArgs()[3], "running");
|
||||
assertEquals(selection.getSelectionArgs()[4], "road biking");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_tracksId_and_dateRange() {
|
||||
// given
|
||||
Instant instant = Instant.now();
|
||||
long oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
Track.Id trackId1 = new Track.Id(1);
|
||||
Track.Id trackId2 = new Track.Id(2);
|
||||
Track.Id trackId3 = new Track.Id(3);
|
||||
|
||||
TrackSelection filter = new TrackSelection()
|
||||
.addTrackId(trackId1)
|
||||
.addTrackId(trackId2)
|
||||
.addTrackId(trackId3)
|
||||
.addDateRange(instant, instant.plusMillis(oneDay));
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "_id IN (?,?,?) AND starttime BETWEEN ? AND ?");
|
||||
assertEquals(selection.getSelectionArgs().length, 5);
|
||||
assertEquals(selection.getSelectionArgs()[0], "1");
|
||||
assertEquals(selection.getSelectionArgs()[1], "2");
|
||||
assertEquals(selection.getSelectionArgs()[2], "3");
|
||||
assertEquals(selection.getSelectionArgs()[3], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selection.getSelectionArgs()[4], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFilterBuildSelection_categories_and_dateRange() {
|
||||
// given
|
||||
Instant instant = Instant.now();
|
||||
long oneDay = 24 * 60 * 60 * 1000;
|
||||
|
||||
TrackSelection filter = new TrackSelection()
|
||||
.addCategory("running")
|
||||
.addCategory("road biking")
|
||||
.addDateRange(instant, instant.plusMillis(oneDay));
|
||||
|
||||
// when
|
||||
SelectionData selection = filter.buildSelection();
|
||||
|
||||
// Then
|
||||
assertEquals(selection.getSelection(), "category IN (?,?) AND starttime BETWEEN ? AND ?");
|
||||
assertEquals(selection.getSelectionArgs().length, 4);
|
||||
assertEquals(selection.getSelectionArgs()[0], "running");
|
||||
assertEquals(selection.getSelectionArgs()[1], "road biking");
|
||||
assertEquals(selection.getSelectionArgs()[2], Long.toString(instant.toEpochMilli()));
|
||||
assertEquals(selection.getSelectionArgs()[3], Long.toString(instant.toEpochMilli() + oneDay));
|
||||
}
|
||||
}
|
||||
@@ -1,35 +1,61 @@
|
||||
package de.dennisguse.opentracks;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.adapters.AggregatedStatisticsAdapter;
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackSelection;
|
||||
import de.dennisguse.opentracks.databinding.AggregatedStatsBinding;
|
||||
import de.dennisguse.opentracks.fragments.FilterDialogFragment;
|
||||
import de.dennisguse.opentracks.viewmodels.AggregatedStatisticsModel;
|
||||
|
||||
public class AggregatedStatisticsActivity extends AbstractActivity {
|
||||
public class AggregatedStatisticsActivity extends AbstractActivity implements FilterDialogFragment.FilterDialogListener {
|
||||
|
||||
public static final String EXTRA_TRACK_IDS = "track_ids";
|
||||
|
||||
static final String STATE_ARE_FILTERS_APPLIED = "areFiltersApplied";
|
||||
|
||||
private AggregatedStatsBinding viewBinding;
|
||||
|
||||
private AggregatedStatisticsAdapter adapter;
|
||||
|
||||
private AggregatedStatisticsModel viewModel;
|
||||
private final TrackSelection selection = new TrackSelection();
|
||||
|
||||
private boolean areFiltersApplied;
|
||||
private MenuItem filterItem;
|
||||
private MenuItem clearFilterItem;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
viewBinding.aggregatedStatsList.setEmptyView(viewBinding.aggregatedStatsEmptyView);
|
||||
|
||||
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||
areFiltersApplied = savedInstanceState != null && savedInstanceState.getBoolean(STATE_ARE_FILTERS_APPLIED);
|
||||
|
||||
final AggregatedStatisticsModel viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
||||
viewModel.getAggregatedStats(trackIds).observe(this, aggregatedStatistics -> {
|
||||
List<Track.Id> trackIds = getIntent().getParcelableArrayListExtra(EXTRA_TRACK_IDS);
|
||||
if (trackIds != null && !trackIds.isEmpty()) {
|
||||
trackIds.stream().forEach(selection::addTrackId);
|
||||
}
|
||||
|
||||
viewModel = new ViewModelProvider(this).get(AggregatedStatisticsModel.class);
|
||||
viewModel.getAggregatedStats(selection).observe(this, aggregatedStatistics -> {
|
||||
if ((aggregatedStatistics == null || aggregatedStatistics.getCount() == 0) && !selection.isEmpty()) {
|
||||
viewBinding.aggregatedStatsEmptyView.setText(getString(R.string.aggregated_stats_filter_no_results));
|
||||
}
|
||||
if (aggregatedStatistics != null) {
|
||||
adapter = new AggregatedStatisticsAdapter(this, aggregatedStatistics);
|
||||
viewBinding.aggregatedStatsList.setAdapter(adapter);
|
||||
@@ -38,9 +64,58 @@ public class AggregatedStatisticsActivity extends AbstractActivity {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(STATE_ARE_FILTERS_APPLIED, areFiltersApplied);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View getRootView() {
|
||||
viewBinding = AggregatedStatsBinding.inflate(getLayoutInflater());
|
||||
return viewBinding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.aggregated_statistics, menu);
|
||||
clearFilterItem = menu.findItem(R.id.aggregated_statistics_clear_filter);
|
||||
filterItem = menu.findItem(R.id.aggregated_statistics_filter);
|
||||
setMenuVisibility(areFiltersApplied);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.aggregated_statistics_filter) {
|
||||
ArrayList<FilterDialogFragment.FilterItem> filterItems = new ArrayList<>();
|
||||
adapter.getCategories().stream().forEach(category -> filterItems.add(new FilterDialogFragment.FilterItem(category, category, true)));
|
||||
FilterDialogFragment.showDialog(getSupportFragmentManager(), filterItems);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.aggregated_statistics_clear_filter) {
|
||||
setMenuVisibility(false);
|
||||
viewModel.clearSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void setMenuVisibility(boolean areFiltersApplied) {
|
||||
this.areFiltersApplied = areFiltersApplied;
|
||||
if (clearFilterItem != null && filterItem != null) {
|
||||
clearFilterItem.setVisible(this.areFiltersApplied);
|
||||
filterItem.setVisible(!this.areFiltersApplied);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilterDone(ArrayList<FilterDialogFragment.FilterItem> filterItems, LocalDateTime from, LocalDateTime to) {
|
||||
setMenuVisibility(true);
|
||||
selection.addDateRange(from.atZone(ZoneId.systemDefault()).toInstant(), to.atZone(ZoneId.systemDefault()).toInstant());
|
||||
filterItems.stream().filter(fi -> fi.isChecked).forEach(fi -> selection.addCategory(fi.value));
|
||||
viewModel.updateSelection(selection);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.settings.PreferencesUtils;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
@@ -17,7 +20,7 @@ import de.dennisguse.opentracks.viewmodels.AggregatedStatistics;
|
||||
|
||||
public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
|
||||
private final AggregatedStatistics aggregatedStatistics;
|
||||
private AggregatedStatistics aggregatedStatistics;
|
||||
private final Context context;
|
||||
|
||||
public AggregatedStatisticsAdapter(Context context, AggregatedStatistics aggregatedStatistics) {
|
||||
@@ -66,6 +69,20 @@ public class AggregatedStatisticsAdapter extends BaseAdapter {
|
||||
return convertView;
|
||||
}
|
||||
|
||||
public AggregatedStatistics swapData(AggregatedStatistics aggregatedStatistics) {
|
||||
this.aggregatedStatistics = aggregatedStatistics;
|
||||
this.notifyDataSetChanged();
|
||||
return aggregatedStatistics;
|
||||
}
|
||||
|
||||
public List<String> getCategories() {
|
||||
List<String> categories = new ArrayList<>();
|
||||
for (int i = 0; i < aggregatedStatistics.getCount(); i++) {
|
||||
categories.add(aggregatedStatistics.getItem(i).getCategory());
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
|
||||
private class ViewHolder {
|
||||
private final ImageView sportIcon;
|
||||
private final TextView typeLabel;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package de.dennisguse.opentracks.content.data;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
import de.dennisguse.opentracks.content.provider.SelectionData;
|
||||
|
||||
public class TrackSelection implements ContentProviderUtils.ContentProviderSelectionInterface {
|
||||
private final List<Track.Id> trackIds = new ArrayList<>();
|
||||
private final List<String> categories = new ArrayList<>();
|
||||
private Instant from;
|
||||
private Instant to;
|
||||
|
||||
public TrackSelection addDateRange(Instant from, Instant to) {
|
||||
this.from = from;
|
||||
this.to = to;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackSelection addTrackId(Track.Id trackId) {
|
||||
if (!this.trackIds.contains(trackId)) {
|
||||
this.trackIds.add(trackId);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public TrackSelection addCategory(String category) {
|
||||
if (!this.categories.contains(category)) {
|
||||
this.categories.add(category);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return trackIds.isEmpty() && categories.isEmpty() && from == null && to == null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SelectionData buildSelection() {
|
||||
String selection = "";
|
||||
String[] selectionArgs;
|
||||
ArrayList<String> fromToArgs = new ArrayList<>();
|
||||
|
||||
// Builds selection.
|
||||
if (!trackIds.isEmpty()) {
|
||||
selection = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||
}
|
||||
if (!categories.isEmpty()) {
|
||||
selection += selection.isEmpty() ? "" : " AND ";
|
||||
selection += String.format(TracksColumns.CATEGORY + " IN (%s)", TextUtils.join(",", Collections.nCopies(categories.size(), "?")));
|
||||
}
|
||||
if (from != null && to != null) {
|
||||
selection += selection.isEmpty() ? "" : " AND ";
|
||||
selection += TracksColumns.STARTTIME + " BETWEEN ? AND ?";
|
||||
fromToArgs.add(Long.toString(from.toEpochMilli()));
|
||||
fromToArgs.add(Long.toString(to.toEpochMilli()));
|
||||
}
|
||||
|
||||
if (selection.isEmpty()) {
|
||||
return new SelectionData();
|
||||
}
|
||||
|
||||
// Builds selection arguments.
|
||||
ArrayList<String> args = trackIds.stream().map(id -> Long.toString(id.getId())).collect(Collectors.toCollection(ArrayList::new));
|
||||
args.addAll(categories);
|
||||
args.addAll(fromToArgs);
|
||||
selectionArgs = args.stream().toArray(String[]::new);
|
||||
|
||||
return new SelectionData(selection, selectionArgs);
|
||||
}
|
||||
}
|
||||
@@ -73,6 +73,10 @@ public class ContentProviderUtils {
|
||||
|
||||
private final ContentResolver contentResolver;
|
||||
|
||||
public interface ContentProviderSelectionInterface {
|
||||
SelectionData buildSelection();
|
||||
}
|
||||
|
||||
public ContentProviderUtils(Context context) {
|
||||
contentResolver = context.getContentResolver();
|
||||
}
|
||||
@@ -205,15 +209,10 @@ public class ContentProviderUtils {
|
||||
return tracks;
|
||||
}
|
||||
|
||||
public List<Track> getTracks(List<Track.Id> trackIds) {
|
||||
if (trackIds == null || trackIds.isEmpty()) {
|
||||
return getTracks();
|
||||
}
|
||||
|
||||
String selection = String.format(TracksColumns._ID + " IN (%s)", TextUtils.join(",", Collections.nCopies(trackIds.size(), "?")));
|
||||
String[] selectionArgs = trackIds.stream().map(id -> Long.toString(id.getId())).toArray(String[]::new);
|
||||
public List<Track> getTracks(ContentProviderSelectionInterface selection) {
|
||||
SelectionData selectionData = selection.buildSelection();
|
||||
ArrayList<Track> tracks = new ArrayList<>();
|
||||
try (Cursor cursor = getTrackCursor(selection, selectionArgs, TracksColumns._ID)) {
|
||||
try (Cursor cursor = getTrackCursor(selectionData.getSelection(), selectionData.getSelectionArgs(), TracksColumns._ID)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
tracks.ensureCapacity(cursor.getCount());
|
||||
do {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.dennisguse.opentracks.content.provider;
|
||||
|
||||
public class SelectionData {
|
||||
private final String selection;
|
||||
private final String[] selectionArgs;
|
||||
|
||||
public SelectionData() {
|
||||
selection = null;
|
||||
selectionArgs = null;
|
||||
}
|
||||
|
||||
public SelectionData(String selection, String[] selectionArgs) {
|
||||
this.selection = selection;
|
||||
this.selectionArgs = selectionArgs;
|
||||
}
|
||||
|
||||
public String getSelection() {
|
||||
return selection;
|
||||
}
|
||||
|
||||
public String[] getSelectionArgs() {
|
||||
return selectionArgs;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package de.dennisguse.opentracks.fragments;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.GridLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.WeekFields;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Locale;
|
||||
|
||||
import de.dennisguse.opentracks.R;
|
||||
import de.dennisguse.opentracks.util.StringUtils;
|
||||
|
||||
public class FilterDialogFragment extends DialogFragment {
|
||||
|
||||
private static final String TAG = FilterDialogFragment.class.getSimpleName();
|
||||
public static final String KEY_FILTER_ITEMS = "filterItems";
|
||||
|
||||
private FilterDialogListener filterDialogListener;
|
||||
private ArrayList<FilterItem> filterItems = new ArrayList<>();
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager) {
|
||||
FilterDialogFragment filterDialogFragment = new FilterDialogFragment();
|
||||
filterDialogFragment.show(fragmentManager, TAG);
|
||||
}
|
||||
|
||||
public static void showDialog(FragmentManager fragmentManager, ArrayList<FilterItem> items) {
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelableArrayList(KEY_FILTER_ITEMS, items);
|
||||
|
||||
FilterDialogFragment filterDialogFragment = new FilterDialogFragment();
|
||||
filterDialogFragment.setArguments(bundle);
|
||||
filterDialogFragment.show(fragmentManager, TAG);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
filterItems = getArguments().getParcelableArrayList(KEY_FILTER_ITEMS);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(getString(R.string.generic_filter));
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getActivity());
|
||||
View layout = inflater.inflate(R.layout.fragment_filter_dialog, null, false);
|
||||
GridLayout itemsLayout = layout.findViewById(R.id.filter_items);
|
||||
builder.setView(layout);
|
||||
|
||||
for (FilterItem item : filterItems) {
|
||||
View view = inflater.inflate(R.layout.fragment_filter_dialog_item, null);
|
||||
|
||||
CheckBox checkBox = view.findViewById(R.id.filter_dialog_check_button);
|
||||
checkBox.setText(item.value);
|
||||
checkBox.setChecked(item.isChecked);
|
||||
checkBox.setTag(item.id);
|
||||
checkBox.setOnClickListener(v -> item.isChecked = !item.isChecked);
|
||||
|
||||
itemsLayout.addView(view);
|
||||
}
|
||||
|
||||
DatePicker datePickerFrom = layout.findViewById(R.id.filter_date_picker_from);
|
||||
DatePicker datePickerTo = layout.findViewById(R.id.filter_date_picker_to);
|
||||
TextInputEditText dateFrom = layout.findViewById(R.id.filter_date_edit_text_from);
|
||||
TextInputEditText dateTo = layout.findViewById(R.id.filter_date_edit_text_to);
|
||||
|
||||
LocalDateTime firstDayThisWeek = LocalDate.now().with(WeekFields.of(Locale.getDefault()).getFirstDayOfWeek()).atStartOfDay();
|
||||
dateFrom.setText(StringUtils.formatDate(getActivity(), firstDayThisWeek));
|
||||
datePickerFrom.init(firstDayThisWeek.getYear(), firstDayThisWeek.getMonthValue() - 1, firstDayThisWeek.getDayOfMonth(), (view, year, monthOfYear, dayOfMonth) -> {
|
||||
LocalDateTime localDateTime = LocalDateTime.of(year, monthOfYear + 1, dayOfMonth, 0, 0, 0);
|
||||
dateFrom.setText(StringUtils.formatDate(getActivity(), localDateTime));
|
||||
datePickerFrom.setVisibility(View.GONE);
|
||||
datePickerTo.setMinDate(localDateTime.toInstant(ZoneOffset.ofTotalSeconds(0)).toEpochMilli());
|
||||
if (localDateTime.isAfter(LocalDateTime.of(datePickerTo.getYear(), datePickerTo.getMonth() + 1, datePickerTo.getDayOfMonth(), 23, 59, 59))) {
|
||||
datePickerTo.updateDate(year, monthOfYear, dayOfMonth);
|
||||
}
|
||||
});
|
||||
|
||||
LocalDateTime lastDayThisWeek = firstDayThisWeek.plusDays(6).withHour(23).withMinute(59).withSecond(59);
|
||||
dateTo.setText(StringUtils.formatDate(getActivity(), lastDayThisWeek));
|
||||
datePickerTo.init(lastDayThisWeek.getYear(), lastDayThisWeek.getMonthValue() - 1, lastDayThisWeek.getDayOfMonth(), (view, year, monthOfYear, dayOfMonth) -> {
|
||||
LocalDateTime localDateTime = LocalDateTime.of(year, monthOfYear + 1, dayOfMonth, 23, 59, 59);
|
||||
dateTo.setText(StringUtils.formatDate(getActivity(), localDateTime));
|
||||
datePickerTo.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
dateFrom.setOnClickListener(v -> {
|
||||
datePickerFrom.setVisibility(View.VISIBLE);
|
||||
datePickerTo.setVisibility(View.GONE);
|
||||
});
|
||||
|
||||
dateTo.setOnClickListener(v -> {
|
||||
datePickerFrom.setVisibility(View.GONE);
|
||||
datePickerTo.setVisibility(View.VISIBLE);
|
||||
});
|
||||
|
||||
builder.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
filterDialogListener.onFilterDone(
|
||||
filterItems,
|
||||
LocalDateTime.of(datePickerFrom.getYear(), datePickerFrom.getMonth() + 1, datePickerFrom.getDayOfMonth(), 0, 0, 0),
|
||||
LocalDateTime.of(datePickerTo.getYear(), datePickerTo.getMonth() + 1, datePickerTo.getDayOfMonth(), 23, 59, 59)
|
||||
);
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(@NonNull Context context) {
|
||||
super.onAttach(context);
|
||||
try {
|
||||
filterDialogListener = (FilterDialogListener) context;
|
||||
} catch (ClassCastException e) {
|
||||
throw new ClassCastException(context + " must implement " + FilterDialogListener.class.getSimpleName());
|
||||
}
|
||||
}
|
||||
|
||||
public interface FilterDialogListener {
|
||||
void onFilterDone(ArrayList<FilterItem> filters, LocalDateTime from, LocalDateTime to);
|
||||
}
|
||||
|
||||
public static class FilterItem implements Parcelable {
|
||||
public final String id;
|
||||
public final String value;
|
||||
public boolean isChecked;
|
||||
|
||||
public FilterItem(String id, String value) {
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.isChecked = true;
|
||||
}
|
||||
|
||||
public FilterItem(String id, String value, boolean isChecked) {
|
||||
this.id = id;
|
||||
this.value = value;
|
||||
this.isChecked = isChecked;
|
||||
}
|
||||
|
||||
protected FilterItem(Parcel in) {
|
||||
id = in.readString();
|
||||
value = in.readString();
|
||||
isChecked = in.readByte() != 0;
|
||||
}
|
||||
|
||||
public static final Creator<FilterItem> CREATOR = new Creator<FilterItem>() {
|
||||
@Override
|
||||
public FilterItem createFromParcel(Parcel in) {
|
||||
return new FilterItem(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FilterItem[] newArray(int size) {
|
||||
return new FilterItem[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(id);
|
||||
dest.writeString(value);
|
||||
dest.writeByte((byte) (isChecked ? 1 : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import java.text.DecimalFormat;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
@@ -61,6 +62,13 @@ public class StringUtils {
|
||||
+ " " + DateUtils.formatDateTime(context, time.toEpochMilli(), DateUtils.FORMAT_SHOW_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the date and time based on user's phone date/time preferences.
|
||||
*/
|
||||
public static String formatDate(Context context, LocalDateTime localDateTime) {
|
||||
return DateUtils.formatDateTime(context, localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(), DateUtils.FORMAT_SHOW_DATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the time using the ISO 8601 date time format with fractional seconds in UTC time zone.
|
||||
*/
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import java.util.List;
|
||||
|
||||
import de.dennisguse.opentracks.content.data.Track;
|
||||
import de.dennisguse.opentracks.content.data.TrackSelection;
|
||||
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
|
||||
|
||||
public class AggregatedStatisticsModel extends AndroidViewModel {
|
||||
@@ -21,18 +22,26 @@ public class AggregatedStatisticsModel extends AndroidViewModel {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable List<Track.Id> trackIds) {
|
||||
public LiveData<AggregatedStatistics> getAggregatedStats(@Nullable TrackSelection selection) {
|
||||
if (aggregatedStats == null) {
|
||||
aggregatedStats = new MutableLiveData<>();
|
||||
loadAggregatedStats(trackIds);
|
||||
loadAggregatedStats(selection);
|
||||
}
|
||||
return aggregatedStats;
|
||||
}
|
||||
|
||||
private void loadAggregatedStats(@Nullable List<Track.Id> trackIds) {
|
||||
public void updateSelection(TrackSelection selection) {
|
||||
loadAggregatedStats(selection);
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
loadAggregatedStats(new TrackSelection());
|
||||
}
|
||||
|
||||
private void loadAggregatedStats(TrackSelection selection) {
|
||||
new Thread(() -> {
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtils(getApplication().getApplicationContext());
|
||||
List<Track> tracks = contentProviderUtils.getTracks(trackIds);
|
||||
List<Track> tracks = selection != null ? contentProviderUtils.getTracks(selection) : contentProviderUtils.getTracks();
|
||||
|
||||
AggregatedStatistics aggregatedStatistics = new AggregatedStatistics(tracks);
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,11L7,11v2h2v-2zM13,11h-2v2h2v-2zM17,11h-2v2h2v-2zM19,4h-1L18,2h-2v2L8,4L8,2L6,2v2L5,4c-1.11,0 -1.99,0.9 -1.99,2L3,20c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,6c0,-1.1 -0.9,-2 -2,-2zM19,20L5,20L5,9h14v11z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,14 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:name="vector"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:name="path"
|
||||
android:pathData="M 5.039 4 C 4.209 4 3.74 4.949 4.25 5.609 C 6.086 7.964 8.901 11.587 9.477 12.328 L 17.805 4 L 5.039 4 Z M 14 13.959 L 10 17.959 L 10 19 C 10 19.55 10.45 20 11 20 L 13 20 C 13.55 20 14 19.55 14 19 L 14 13.959 Z M 5.415 18.466 L 19.284 4.597 C 19.449 4.432 19.673 4.339 19.906 4.339 C 20.14 4.339 20.364 4.432 20.529 4.597 L 20.529 4.597 C 20.694 4.762 20.787 4.986 20.787 5.219 C 20.787 5.453 20.694 5.677 20.529 5.842 L 6.66 19.711 C 6.495 19.876 6.271 19.968 6.037 19.968 C 5.804 19.968 5.58 19.876 5.415 19.711 C 5.306 19.601 5.227 19.465 5.187 19.316 C 5.147 19.167 5.147 19.009 5.187 18.86 C 5.227 18.711 5.306 18.575 5.415 18.466"
|
||||
android:fillColor="@android:color/white"
|
||||
android:strokeWidth="1"/>
|
||||
</vector>
|
||||
@@ -16,6 +16,7 @@
|
||||
style="@style/TextLarge"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp"
|
||||
android:gravity="center_horizontal|center_vertical"
|
||||
android:text="@string/aggregated_stats_empty_message" />
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView style="@style/ScrollView.WithBottomButtons">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp">
|
||||
|
||||
<GridLayout
|
||||
android:id="@+id/filter_items"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:columnCount="2"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:alignmentMode="alignBounds"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/filter_date_from" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/filter_date_from"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/generic_from"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/filter_items"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/filter_date_picker_from">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filter_date_edit_text_from"
|
||||
android:drawableStart="@drawable/ic_baseline_date_range_24"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="true"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:inputType="date"
|
||||
android:selectAllOnFocus="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<DatePicker
|
||||
android:id="@+id/filter_date_picker_from"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/filter_date_from"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/filter_date_to" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/filter_date_to"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/generic_to"
|
||||
android:imeOptions="actionNext"
|
||||
android:importantForAutofill="no"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/filter_date_picker_from"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/filter_date_picker_to">
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/filter_date_edit_text_to"
|
||||
android:drawableStart="@drawable/ic_baseline_date_range_24"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:enabled="true"
|
||||
android:focusable="false"
|
||||
android:focusableInTouchMode="false"
|
||||
android:inputType="date"
|
||||
android:selectAllOnFocus="true" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<DatePicker
|
||||
android:id="@+id/filter_date_picker_to"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:paddingLeft="8dp"
|
||||
android:paddingRight="8dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/filter_date_to"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/filter_dialog_check_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
tools:text="Check Button" />
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/aggregated_statistics_clear_filter"
|
||||
android:icon="@drawable/ic_clear_filter_24dp"
|
||||
app:showAsAction="always"
|
||||
android:title="@string/menu_filter" />
|
||||
<item
|
||||
android:id="@+id/aggregated_statistics_filter"
|
||||
android:icon="@drawable/ic_baseline_filter_alt_24"
|
||||
app:showAsAction="always"
|
||||
android:title="@string/menu_filter" />
|
||||
</menu>
|
||||
@@ -239,6 +239,9 @@ limitations under the License.
|
||||
<string name="generic_click_twice_cancel">Click back again to cancel</string>
|
||||
<string name="generic_choose_an_option">Choose an option</string>
|
||||
<string name="generic_undo">Undo</string>
|
||||
<string name="generic_filter">Filter</string>
|
||||
<string name="generic_from">From</string>
|
||||
<string name="generic_to">To</string>
|
||||
<!-- Gps -->
|
||||
<string name="gps_starting">Starting GPS</string>
|
||||
<string name="gps_wait_for_better_signal">Waiting for a better GPS signal</string>
|
||||
@@ -306,6 +309,7 @@ limitations under the License.
|
||||
<string name="menu_voice_rate">Voice speed</string>
|
||||
<string name="menu_resume_track">Resume track</string>
|
||||
<string name="menu_add_custom_layout">New Layout</string>
|
||||
<string name="menu_filter">Filter</string>
|
||||
<!-- OpenTracks -->
|
||||
<string name="recording_service">Recording service</string>
|
||||
<string name="delete_service">Deletion service</string>
|
||||
@@ -491,6 +495,7 @@ limitations under the License.
|
||||
<string name="track_list_empty_message">Start recording your next adventure here</string>
|
||||
<!-- Aggregated Statistics -->
|
||||
<string name="aggregated_stats_empty_message">Record your first track to see aggregated statistics</string>
|
||||
<string name="aggregated_stats_filter_no_results">There are not activities for the specified filter</string>
|
||||
<!-- Interval List -->
|
||||
<string name="interval_list_empty_message">There are not any interval yet</string>
|
||||
<!-- Unit -->
|
||||
|
||||
Reference in New Issue
Block a user