diff --git a/app/res/values/pref_keys.xml b/app/res/values/pref_keys.xml index d4b2bc169..f1160aac1 100644 --- a/app/res/values/pref_keys.xml +++ b/app/res/values/pref_keys.xml @@ -22,6 +22,7 @@ pref_countdown_time pref_unit pref_speedunit + pref_speedunit_opposite_sports pref_startgps pref_pollInterval pref_pollDistance diff --git a/app/res/values/strings.xml b/app/res/values/strings.xml index e97cba835..0649455ef 100644 --- a/app/res/values/strings.xml +++ b/app/res/values/strings.xml @@ -18,4 +18,6 @@ + - 3 + Activities using opposite speed unit + No activity overrides. All activities use the global speed unit. diff --git a/app/res/xml/settings_units.xml b/app/res/xml/settings_units.xml index 0a93aa6d5..d70c38c7a 100644 --- a/app/res/xml/settings_units.xml +++ b/app/res/xml/settings_units.xml @@ -37,4 +37,10 @@ android:title="@string/Speed_unit_preference" app:iconSpaceReserved="false" /> + + diff --git a/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java b/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java index c77f716db..a5fab6c8d 100644 --- a/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java +++ b/app/src/main/org/runnerup/export/RunnerUpLiveSynchronizer.java @@ -198,7 +198,8 @@ public void workoutEvent(WorkoutInfo workoutInfo, int type) { LiveService.PARAM_IN_PACE, formatter.formatVelocityByPreferredUnit( Formatter.Format.TXT_SHORT, - elapsedTimeMillis == 0 ? 0 : elapsedDistanceMeter * 1000.0 / elapsedTimeMillis)) + elapsedTimeMillis == 0 ? 0 : elapsedDistanceMeter * 1000.0 / elapsedTimeMillis, + workoutInfo.getSport())) .putExtra(LiveService.PARAM_IN_USERNAME, username) .putExtra(LiveService.PARAM_IN_PASSWORD, password) .putExtra(LiveService.PARAM_IN_SERVERADRESS, postUrl); diff --git a/app/src/main/org/runnerup/notification/OngoingState.java b/app/src/main/org/runnerup/notification/OngoingState.java index 7cc9ef0ce..670e6299b 100644 --- a/app/src/main/org/runnerup/notification/OngoingState.java +++ b/app/src/main/org/runnerup/notification/OngoingState.java @@ -79,7 +79,7 @@ public Notification createNotification() { Formatter.Format.TXT_LONG, Math.round(workoutInfo.getTime(Scope.ACTIVITY))); String pace = formatter.formatVelocityByPreferredUnit( - Formatter.Format.TXT_SHORT, workoutInfo.getSpeed(Scope.ACTIVITY)); + Formatter.Format.TXT_SHORT, workoutInfo.getSpeed(Scope.ACTIVITY), workoutInfo.getSport()); String content = String.format( @@ -88,7 +88,7 @@ public Notification createNotification() { distance, context.getString(org.runnerup.common.R.string.time), time, - context.getString(org.runnerup.common.R.string.pace), + formatter.formatVelocityLabel(workoutInfo.getSport()), pace); builder.setContentText(content); diff --git a/app/src/main/org/runnerup/util/Formatter.java b/app/src/main/org/runnerup/util/Formatter.java index 82487a1b1..d6f400108 100644 --- a/app/src/main/org/runnerup/util/Formatter.java +++ b/app/src/main/org/runnerup/util/Formatter.java @@ -33,6 +33,7 @@ import java.util.Locale; import org.runnerup.R; import org.runnerup.common.util.Constants; +import org.runnerup.common.util.Constants.DB; import org.runnerup.workout.Dimension; import org.runnerup.workout.SpeedUnit; @@ -217,7 +218,19 @@ private static boolean guessDefaultUnit(Resources res, Editor editor) { * @return Configured Speed Unit (falls back to pace, if the configured value is invalid) */ public static SpeedUnit getPreferredSpeedUnit(Context context) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + return getGlobalPreferredSpeedUnit( + context, PreferenceManager.getDefaultSharedPreferences(context)); + } + + public static SpeedUnit getPreferredSpeedUnit(Context context, int sport) { + SpeedUnit globalSpeedUnit = getPreferredSpeedUnit(context); + if (!usesOppositeSpeedUnit(context, sport)) { + return globalSpeedUnit; + } + return globalSpeedUnit == SpeedUnit.PACE ? SpeedUnit.SPEED : SpeedUnit.PACE; + } + + private static SpeedUnit getGlobalPreferredSpeedUnit(Context context, SharedPreferences prefs) { // use either pace or speed according to the user's preference String speedUnit = prefs.getString( @@ -232,6 +245,69 @@ public static SpeedUnit getPreferredSpeedUnit(Context context) { } } + public static int[] getOppositeSpeedUnitSports(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + String storedSports = + prefs.getString(context.getString(R.string.pref_speedunit_opposite_sports), null); + if (storedSports == null || storedSports.isEmpty()) { + return new int[0]; + } + return normalizeSportIds(SafeParse.parseIntList(storedSports)); + } + + public static void setOppositeSpeedUnitSports(Context context, int[] sports) { + int[] normalizedSports = normalizeSportIds(sports); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + Editor editor = prefs.edit(); + String key = context.getString(R.string.pref_speedunit_opposite_sports); + if (normalizedSports.length == 0) { + editor.remove(key).apply(); + return; + } + editor.putString(key, SafeParse.storeIntList(normalizedSports)).apply(); + } + + private static boolean usesOppositeSpeedUnit(Context context, int sport) { + if (sport < 0) { + return false; + } + for (int oppositeSport : getOppositeSpeedUnitSports(context)) { + if (oppositeSport == sport) { + return true; + } + } + return false; + } + + private static int[] normalizeSportIds(int[] sports) { + if (sports == null || sports.length == 0) { + return new int[0]; + } + + boolean[] includedSports = new boolean[DB.ACTIVITY.SPORT_MAX + 1]; + for (int sport : sports) { + if (0 <= sport && sport <= DB.ACTIVITY.SPORT_MAX) { + includedSports[sport] = true; + } + } + + int count = 0; + for (boolean includedSport : includedSports) { + if (includedSport) { + count++; + } + } + + int[] normalizedSports = new int[count]; + int index = 0; + for (int sport = 0; sport < includedSports.length; sport++) { + if (includedSports[sport]) { + normalizedSports[index++] = sport; + } + } + return normalizedSports; + } + public double getUnitMeters() { return this.base_meters; } @@ -483,15 +559,14 @@ public String formatPace(Format target, double seconds_per_meter) { * @return display value */ public String formatVelocityByPreferredUnit(Format target, double meters_per_second) { - String paceTextUnit = - this.sharedPreferences.getString( - context.getResources().getString(R.string.pref_speedunit), SpeedUnit.PACE.getValue()); - assert paceTextUnit != null; - if (paceTextUnit.contentEquals(SpeedUnit.PACE.getValue())) { + return formatVelocityByPreferredUnit(target, meters_per_second, -1); + } + + public String formatVelocityByPreferredUnit(Format target, double meters_per_second, int sport) { + if (getPreferredSpeedUnit(context, sport) == SpeedUnit.PACE) { return this.formatPaceSpeed(target, meters_per_second); - } else { - return this.formatSpeed(target, meters_per_second); } + return this.formatSpeed(target, meters_per_second); } /** @@ -500,15 +575,14 @@ public String formatVelocityByPreferredUnit(Format target, double meters_per_sec * @return value */ public String formatVelocityLabel() { - String paceTextUnit = - this.sharedPreferences.getString( - context.getResources().getString(R.string.pref_speedunit), SpeedUnit.PACE.getValue()); - assert paceTextUnit != null; - if (paceTextUnit.contentEquals(SpeedUnit.PACE.getValue())) { + return formatVelocityLabel(-1); + } + + public String formatVelocityLabel(int sport) { + if (getPreferredSpeedUnit(context, sport) == SpeedUnit.PACE) { return this.context.getString(org.runnerup.common.R.string.Pace); - } else { - return this.context.getString(org.runnerup.common.R.string.Speed); } + return this.context.getString(org.runnerup.common.R.string.Speed); } /** @@ -538,11 +612,16 @@ public String formatPaceSpeed(Format target, double meters_per_second) { */ String getVelocityUnit( Context context) { // Resources resources, SharedPreferences sharedPreferences) { + return getVelocityUnit(context, -1); + } + + String getVelocityUnit( + Context context, int sport) { // Resources resources, SharedPreferences sharedPreferences) { int du = metric ? org.runnerup.common.R.string.metrics_distance_km : org.runnerup.common.R.string.metrics_distance_mi; - switch (getPreferredSpeedUnit(context)) { + switch (getPreferredSpeedUnit(context, sport)) { case SPEED: return resources.getString(du) + "/" diff --git a/app/src/main/org/runnerup/util/GraphWrapper.java b/app/src/main/org/runnerup/util/GraphWrapper.java index 0aa5c29a9..3203c85a5 100644 --- a/app/src/main/org/runnerup/util/GraphWrapper.java +++ b/app/src/main/org/runnerup/util/GraphWrapper.java @@ -38,6 +38,7 @@ import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import org.runnerup.R; import org.runnerup.common.util.Constants; import org.runnerup.db.entities.LocationEntity; @@ -58,6 +59,9 @@ public class GraphWrapper implements Constants { private final Handler handler = new Handler(Looper.getMainLooper()); private final XAxis distanceXAxis; private final XAxis timeXAxis; + private int sport; + private volatile int loadGeneration = 0; + private Future pendingGraphLoad; boolean firstLoad = true; boolean useDistanceAsX = true; private XAxis xAxis; @@ -70,10 +74,12 @@ public GraphWrapper( final Formatter formatter, SQLiteDatabase mDB, long mID, + int sport, boolean use_distance_as_x) { this.graphTab = graphTab; this.hrzonesBarLayout = hrzonesBarLayout; this.formatter = formatter; + this.sport = sport; this.distanceXAxis = new XAxis() { @@ -121,7 +127,6 @@ public double getX(double distance, double time_ms) { loadParam = new LoadParam(context, mDB, mID); graphView = new GraphView(context); - graphView.setTitle(formatter.formatVelocityLabel()); graphView .getGridLabelRenderer() .setLabelFormatter( @@ -131,12 +136,12 @@ public String formatLabel(double value, boolean isValueX) { if (isValueX) { return xAxis.formatValue(value); } else { - return formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, value); + return formatter.formatVelocityByPreferredUnit( + Formatter.Format.TXT_SHORT, value, GraphWrapper.this.sport); } } }); - graphView.getGridLabelRenderer().setVerticalAxisTitle(formatter.getVelocityUnit(context)); - graphView.getGridLabelRenderer().setHorizontalAxisTitle(xAxis.label()); + updateVelocityAxisLabels(); // enable zoom graphView.getViewport().setScalable(true); graphView.getViewport().setScrollable(true); @@ -158,6 +163,7 @@ public String formatLabel(double value, boolean isValueX) { } } }); + updateXAxisLabels(); graphView2.getViewport().setScalable(true); graphView2.getViewport().setScrollable(true); @@ -175,56 +181,114 @@ public void setUseDistanceAsX(boolean val) { } else { xAxis = timeXAxis; } + updateXAxisLabels(); graphView.removeAllSeries(); graphView2.removeAllSeries(); loadGraph(); } + public void setSport(int sport) { + if (this.sport == sport) { + return; + } + this.sport = sport; + updateVelocityAxisLabels(); + graphView.removeAllSeries(); + graphView2.removeAllSeries(); + loadGraph(); + } + + private void updateVelocityAxisLabels() { + graphView.setTitle(formatter.formatVelocityLabel(sport)); + graphView + .getGridLabelRenderer() + .setVerticalAxisTitle(formatter.getVelocityUnit(graphView.getContext(), sport)); + } + + private void updateXAxisLabels() { + final String axisLabel = xAxis.label(); + graphView.getGridLabelRenderer().setHorizontalAxisTitle(axisLabel); + graphView2.getGridLabelRenderer().setHorizontalAxisTitle(axisLabel); + } + private void loadGraph() { - executor.execute( + final GraphLoadRequest request = new GraphLoadRequest(++loadGeneration, sport, xAxis); + if (pendingGraphLoad != null) { + pendingGraphLoad.cancel(true); + } + pendingGraphLoad = + executor.submit( () -> { + if (isObsoleteRequest(request)) { + return; + } // Background work - GraphProducer producer = doLoadGraphInBackground(loadParam); + GraphProducer producer = doLoadGraphInBackground(loadParam, request); + if (producer == null || isObsoleteRequest(request)) { + return; + } // Post result to UI thread - handler.post(() -> onPostExecute(producer)); + handler.post(() -> onPostExecute(request, producer)); }); } - private GraphProducer doLoadGraphInBackground(LoadParam params) { + private GraphProducer doLoadGraphInBackground(LoadParam params, GraphLoadRequest request) { + if (isObsoleteRequest(request)) { + return null; + } LocationEntity.LocationList ll = new LocationEntity.LocationList<>(params.mDB, params.mID); - GraphProducer graphData = new GraphProducer(params.context, ll.getCount()); - double lastDistance = 0; - long lastTime = 0; - int lastLap = -1; - Double tot_distance = 0.0; - double tot_time = 0.0; - for (LocationEntity loc : ll) { - Long time = loc.getElapsed(); - time = time != null ? time : lastTime; - tot_time = time.doubleValue(); - Integer lap = loc.getLap(); - lap = lap != null ? lap : 0; - tot_distance = tot_distance != null ? loc.getDistance() : lastDistance; - - double tot_X = xAxis.getX(tot_distance, time); - if (lap != lastLap) { - graphData.clearSmooth(tot_X); - lastLap = lap; - } + try { + GraphProducer graphData = new GraphProducer(params.context, ll.getCount(), request.sport()); + double lastDistance = 0; + long lastTime = 0; + int lastLap = -1; + Double tot_distance = 0.0; + double tot_time = 0.0; + for (LocationEntity loc : ll) { + if (isObsoleteRequest(request)) { + return null; + } - graphData.addObservation(time - lastTime, tot_distance - lastDistance, tot_X, loc); - lastTime = time; - lastDistance = tot_distance; + Long time = loc.getElapsed(); + time = time != null ? time : lastTime; + tot_time = time.doubleValue(); + Integer lap = loc.getLap(); + lap = lap != null ? lap : 0; + tot_distance = tot_distance != null ? loc.getDistance() : lastDistance; + + double tot_X = request.xAxis().getX(tot_distance, time); + if (lap != lastLap) { + graphData.clearSmooth(tot_X); + lastLap = lap; + } + + graphData.addObservation(time - lastTime, tot_distance - lastDistance, tot_X, loc); + lastTime = time; + lastDistance = tot_distance; + } + if (isObsoleteRequest(request)) { + return null; + } + graphData.clearSmooth(request.xAxis().getX(tot_distance, tot_time)); + return graphData; + } finally { + ll.close(); } - graphData.clearSmooth(xAxis.getX(tot_distance, tot_time)); + } - ll.close(); - return graphData; + private boolean isObsoleteRequest(GraphLoadRequest request) { + return Thread.currentThread().isInterrupted() + || request.generation() != loadGeneration + || request.sport() != sport + || request.xAxis() != xAxis; } - private void onPostExecute(GraphProducer graphData) { + private void onPostExecute(GraphLoadRequest request, GraphProducer graphData) { if (graphData == null) return; + if (request.generation() != loadGeneration) return; + if (request.sport() != sport) return; + if (request.xAxis() != xAxis) return; graphData.complete(graphView); graphTab.removeView(graphView); @@ -302,7 +366,7 @@ class GraphProducer { boolean showHR = false; boolean showHRZhist = false; - public GraphProducer(Context context, int noPoints) { + public GraphProducer(Context context, int noPoints, int sport) { final int GRAPH_INTERVAL_SECONDS = 5; // 1 point every 5 sec final int GRAPH_AVERAGE_SECONDS = 30; // moving average 30 sec @@ -331,7 +395,7 @@ public GraphProducer(Context context, int noPoints) { Arrays.fill(this.hrzHist, 0); showHRZhist = true; } - this.preferred_speedunit = Formatter.getPreferredSpeedUnit(context); + this.preferred_speedunit = Formatter.getPreferredSpeedUnit(context, sport); clearSmooth(0); } @@ -503,10 +567,10 @@ public void complete(final GraphView graphView) { "%s: %s\n%s: %s %s", graphView.getContext().getString(org.runnerup.common.R.string.Distance), xAxis.formatValue(dataPoint.getX()), - formatter.formatVelocityLabel(), + formatter.formatVelocityLabel(sport), formatter.formatVelocityByPreferredUnit( - Formatter.Format.TXT_SHORT, dataPoint.getY()), - formatter.getVelocityUnit(graphView.getContext())); + Formatter.Format.TXT_SHORT, dataPoint.getY(), sport), + formatter.getVelocityUnit(graphView.getContext(), sport)); Toast.makeText(graphView.getContext(), msg, Toast.LENGTH_SHORT).show(); }); if (showHR) { @@ -692,4 +756,6 @@ void KolmogorovZurbenko(int n, int len) { } record LoadParam(Context context, SQLiteDatabase mDB, long mID) {} + + record GraphLoadRequest(int generation, int sport, XAxis xAxis) {} } diff --git a/app/src/main/org/runnerup/view/DetailActivity.java b/app/src/main/org/runnerup/view/DetailActivity.java index f53d9a4eb..727950534 100644 --- a/app/src/main/org/runnerup/view/DetailActivity.java +++ b/app/src/main/org/runnerup/view/DetailActivity.java @@ -121,7 +121,7 @@ public class DetailActivity extends AppCompatActivity implements Constants { private View graphTab; private MapWrapper mapWrapper = null; - private final GraphWrapper graphWrapper = null; + private GraphWrapper graphWrapper = null; private SyncManager syncManager = null; private Formatter formatter = null; @@ -186,6 +186,7 @@ public int preSetValue(int newValue) throws IllegalArgumentException { updateViewForSport(newValue); ViewCompat.requestApplyInsets(rootView); headerData.put(DB.ACTIVITY.SPORT, newValue); + refreshVelocityDisplays(newValue); return newValue; } }); @@ -317,8 +318,16 @@ public WindowInsetsCompat onApplyWindowInsets( LinearLayout hrzonesBarLayout = findViewById(R.id.hrzonesBarLayout); boolean use_distance_as_x = !Sport.isWithoutGps(sport.getValueInt()); // variable not needed - new GraphWrapper(this, graphTabLayout, hrzonesBarLayout, - formatter, mDB, mID, use_distance_as_x); + graphWrapper = + new GraphWrapper( + this, + graphTabLayout, + hrzonesBarLayout, + formatter, + mDB, + mID, + sport.getValueInt(), + use_distance_as_x); if (this.mode == MODE_SAVE) { resumeButton.setOnClickListener(resumeButtonClick); @@ -361,10 +370,36 @@ private void updateViewForSport(int sportValue) { } if (graphWrapper != null) { boolean use_distance_as_x = !Sport.isWithoutGps(sportValue); + graphWrapper.setSport(sportValue); graphWrapper.setUseDistanceAsX(use_distance_as_x); } } + private int getActivitySport(ContentValues data) { + if (data.containsKey(DB.ACTIVITY.SPORT)) { + return data.getAsInteger(DB.ACTIVITY.SPORT); + } + if (sport != null) { + return sport.getValueInt(); + } + return DB.ACTIVITY.SPORT_RUNNING; + } + + private void refreshVelocityDisplays(int sportValue) { + if (headerData.containsKey(DB.ACTIVITY.DISTANCE) && headerData.containsKey(DB.ACTIVITY.TIME)) { + double distance = headerData.getAsDouble(DB.ACTIVITY.DISTANCE); + long time = headerData.getAsLong(DB.ACTIVITY.TIME); + if (time != 0) { + activityPace.setText( + formatter.formatVelocityByPreferredUnit( + Formatter.Format.TXT_LONG, distance / time, sportValue)); + } + } + for (BaseAdapter adapter : adapters) { + adapter.notifyDataSetChanged(); + } + } + private void setUploadVisibility() { boolean enabled = !pendingSynchronizers.isEmpty(); if (enabled) { @@ -628,6 +663,7 @@ private void fillHeaderData() { } private void updateHeader(ContentValues data, boolean fromManualDistance) { + int sportValue = getActivitySport(data); double d = 0; if (data.containsKey(DB.ACTIVITY.DISTANCE)) { d = data.getAsDouble(DB.ACTIVITY.DISTANCE); @@ -661,7 +697,7 @@ private void updateHeader(ContentValues data, boolean fromManualDistance) { activityPace.setVisibility(View.VISIBLE); activityPaceSeparator.setVisibility(View.VISIBLE); activityPace.setText( - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t)); + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t, sportValue)); } else { activityPace.setVisibility(View.GONE); activityPaceSeparator.setVisibility(View.GONE); @@ -745,7 +781,8 @@ public View getView(int position, View convertView, ViewGroup parent) { viewHolder.tv3.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, t)); if (t != 0) { viewHolder.tv4.setText( - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t)); + formatter.formatVelocityByPreferredUnit( + Formatter.Format.TXT_LONG, d / t, sport.getValueInt())); } else { viewHolder.tv4.setText(""); } diff --git a/app/src/main/org/runnerup/view/HistoryFragment.java b/app/src/main/org/runnerup/view/HistoryFragment.java index cf544215a..956e452bc 100644 --- a/app/src/main/org/runnerup/view/HistoryFragment.java +++ b/app/src/main/org/runnerup/view/HistoryFragment.java @@ -231,7 +231,7 @@ public void bindView(View view, Context context, Cursor cursor) { String paceTextContents = ""; if (d != null && dur != null && dur != 0) { paceTextContents = - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / dur); + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / dur, s); } paceText.setText(paceTextContents); } diff --git a/app/src/main/org/runnerup/view/ManualActivity.java b/app/src/main/org/runnerup/view/ManualActivity.java index f653f8d50..21bab460a 100644 --- a/app/src/main/org/runnerup/view/ManualActivity.java +++ b/app/src/main/org/runnerup/view/ManualActivity.java @@ -82,6 +82,7 @@ public void onCreate(Bundle savedInstanceState) { ViewUtil.Insets(findViewById(R.id.tab_manual), true); manualSport.setArrayEntries(Sport.getStringArray(getResources())); + manualSport.setOnSetValueListener(onSetManualSport); } @Override @@ -123,6 +124,10 @@ public void onActivityResult( } void setManualPace(String distance, String duration) { + setManualPace(distance, duration, manualSport.getValueInt()); + } + + void setManualPace(String distance, String duration, int sportValue) { Log.d(getClass().getName(), "distance: >" + distance + "< duration: >" + duration + "<"); double dist = SafeParse.parseDouble(distance, 0); // convert to meters long seconds = SafeParse.parseSeconds(duration, 0); @@ -131,10 +136,26 @@ void setManualPace(String distance, String duration) { return; } manualPace.setValue( - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, dist / seconds)); + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, dist / seconds, sportValue)); manualPace.setVisibility(View.VISIBLE); } + final OnSetValueListener onSetManualSport = + new OnSetValueListener() { + + @Override + public String preSetValue(String newValue) throws IllegalArgumentException { + return newValue; + } + + @Override + public int preSetValue(int newValue) throws IllegalArgumentException { + setManualPace( + manualDistance.getValue().toString(), manualDuration.getValue().toString(), newValue); + return newValue; + } + }; + final OnSetValueListener onSetManualDistance = new OnSetValueListener() { diff --git a/app/src/main/org/runnerup/view/RunActivity.java b/app/src/main/org/runnerup/view/RunActivity.java index 00c2d12b8..98b0fa163 100644 --- a/app/src/main/org/runnerup/view/RunActivity.java +++ b/app/src/main/org/runnerup/view/RunActivity.java @@ -101,6 +101,7 @@ public class RunActivity extends AppCompatActivity implements TickListener { private TextView currentHr; private TextView activityHeaderHr; private TextView hrDebug; + private TextView velocityLabel; // A circular buffer for tap events private final long[] mTapArray = {0, 0, 0, 0}; private int mTapIndex = 0; @@ -123,8 +124,8 @@ public void onCreate(Bundle savedInstanceState) { setContentView(R.layout.run); formatter = new Formatter(this); // HRZones hrZones = new HRZones(this); - TextView velocity = findViewById(R.id.velocity_label); - velocity.setText(formatter.formatVelocityLabel()); + velocityLabel = findViewById(R.id.velocity_label); + velocityLabel.setText(formatter.formatVelocityLabel()); final Button stopButton = findViewById(R.id.stop_button); stopButton.setOnClickListener(stopButtonClick); @@ -265,6 +266,7 @@ private void onGpsTrackerBound() { workout.onBind(workout, bindValues); } + velocityLabel.setText(formatter.formatVelocityLabel(workout.getSport())); startTimer(); populateWorkoutList(); @@ -440,20 +442,24 @@ private void updateView() { } setPauseButtonEnabled(!workout.isPaused()); + int sport = workout.getSport(); + velocityLabel.setText(formatter.formatVelocityLabel(sport)); double ad = workout.getDistance(Scope.ACTIVITY); double at = workout.getTime(Scope.ACTIVITY); double ap = workout.getSpeed(Scope.ACTIVITY); activityTime.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, Math.round(at))); activityDistance.setText( formatter.formatDistance(Formatter.Format.TXT_SHORT, Math.round(ad))); - activityPace.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, ap)); + activityPace.setText( + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, ap, sport)); double ld = workout.getDistance(Scope.LAP); double lt = workout.getTime(Scope.LAP); double lp = workout.getSpeed(Scope.LAP); lapTime.setText(formatter.formatElapsedTime(Formatter.Format.TXT_SHORT, Math.round(lt))); lapDistance.setText(formatter.formatDistance(Formatter.Format.TXT_LONG, Math.round(ld))); - lapPace.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, lp)); + lapPace.setText( + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, lp, sport)); if (tableRowInterval != null && this.currentStep != null @@ -469,14 +475,15 @@ private void updateView() { intervalDistance.setText( formatter.formatDistance(Formatter.Format.TXT_LONG, Math.round(id))); intervalPace.setText( - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, ip)); + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, ip, sport)); } else { // Do not show Interval Step row if no reason tableRowInterval.setVisibility(View.GONE); } double cp = workout.getSpeed(Scope.CURRENT); - currentPace.setText(formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, cp)); + currentPace.setText( + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_SHORT, cp, sport)); if (mTracker.isComponentConnected(TrackerHRM.NAME)) { double ahr = workout.getHeartRate(Scope.ACTIVITY); diff --git a/app/src/main/org/runnerup/view/SettingsUnitsFragment.java b/app/src/main/org/runnerup/view/SettingsUnitsFragment.java index 73f628d20..373c365d7 100644 --- a/app/src/main/org/runnerup/view/SettingsUnitsFragment.java +++ b/app/src/main/org/runnerup/view/SettingsUnitsFragment.java @@ -1,13 +1,133 @@ package org.runnerup.view; +import android.content.SharedPreferences; import android.os.Bundle; +import androidx.appcompat.app.AlertDialog; +import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import org.runnerup.R; +import org.runnerup.common.util.Constants.DB; +import org.runnerup.util.Formatter; +import org.runnerup.workout.Sport; -public class SettingsUnitsFragment extends PreferenceFragmentCompat { +public class SettingsUnitsFragment extends PreferenceFragmentCompat + implements SharedPreferences.OnSharedPreferenceChangeListener { + + private Preference oppositeSpeedUnitSportsPreference; + private String[] sportEntries; @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.settings_units, rootKey); + + sportEntries = Sport.getStringArray(getResources()); + oppositeSpeedUnitSportsPreference = + findPreference(getString(R.string.pref_speedunit_opposite_sports)); + if (oppositeSpeedUnitSportsPreference == null) { + return; + } + + oppositeSpeedUnitSportsPreference.setOnPreferenceClickListener( + preference -> { + showOppositeSportDialog(); + return true; + }); + updateOppositeSportsSummary(); + } + + @Override + public void onResume() { + super.onResume(); + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + updateOppositeSportsSummary(); + } + + @Override + public void onPause() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key != null && key.contentEquals(getString(R.string.pref_speedunit_opposite_sports))) { + updateOppositeSportsSummary(); + } + } + + private void showOppositeSportDialog() { + final boolean[] checked = createCheckedSports(); + + new AlertDialog.Builder(requireContext()) + .setTitle(R.string.speed_unit_opposite_activities_title) + .setMultiChoiceItems( + sportEntries, + checked, + (dialog, which, isChecked) -> { + if (0 <= which && which < checked.length) { + checked[which] = isChecked; + } + }) + .setPositiveButton( + android.R.string.ok, + (dialog, which) -> { + Formatter.setOppositeSpeedUnitSports(requireContext(), getCheckedSportIds(checked)); + updateOppositeSportsSummary(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + private boolean[] createCheckedSports() { + boolean[] checked = new boolean[Math.min(sportEntries.length, DB.ACTIVITY.SPORT_MAX + 1)]; + for (int sportId : Formatter.getOppositeSpeedUnitSports(requireContext())) { + if (0 <= sportId && sportId < checked.length) { + checked[sportId] = true; + } + } + return checked; + } + + private int[] getCheckedSportIds(boolean[] checked) { + int count = 0; + for (boolean isChecked : checked) { + if (isChecked) { + count++; + } + } + + int[] selectedSports = new int[count]; + int index = 0; + for (int sportId = 0; sportId < checked.length; sportId++) { + if (checked[sportId]) { + selectedSports[index++] = sportId; + } + } + return selectedSports; + } + + private void updateOppositeSportsSummary() { + if (oppositeSpeedUnitSportsPreference == null) { + return; + } + + int[] selectedSports = Formatter.getOppositeSpeedUnitSports(requireContext()); + if (selectedSports.length == 0) { + oppositeSpeedUnitSportsPreference.setSummary( + R.string.speed_unit_opposite_activities_summary_empty); + return; + } + + StringBuilder summary = new StringBuilder(); + for (int sportId : selectedSports) { + if (0 > sportId || sportId >= sportEntries.length) { + continue; + } + if (summary.length() > 0) { + summary.append(", "); + } + summary.append(sportEntries[sportId]); + } + oppositeSpeedUnitSportsPreference.setSummary(summary); } } diff --git a/app/src/main/org/runnerup/view/UploadActivity.java b/app/src/main/org/runnerup/view/UploadActivity.java index e6c66179e..26b897c7f 100644 --- a/app/src/main/org/runnerup/view/UploadActivity.java +++ b/app/src/main/org/runnerup/view/UploadActivity.java @@ -351,6 +351,10 @@ public View getView(int arg0, View convertView, ViewGroup parent) { } viewHolder.activityID = getItemId(arg0); SyncActivityItem ai = allSyncActivities.get(arg0); + int sport = + ai.getSport() == null + ? DB.ACTIVITY.SPORT_RUNNING + : Sport.valueOf(ai.getSport()).getDbValue(); Double d = ai.getDistance(); Long t = ai.getDuration(); @@ -376,7 +380,7 @@ public View getView(int arg0, View convertView, ViewGroup parent) { if (d != null && t != null && t != 0) { viewHolder.tvPace.setText( - formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t)); + formatter.formatVelocityByPreferredUnit(Formatter.Format.TXT_LONG, d / t, sport)); } else { viewHolder.tvPace.setText(""); } @@ -384,7 +388,6 @@ public View getView(int arg0, View convertView, ViewGroup parent) { if (ai.getSport() == null) { viewHolder.tvSport.setText(Sport.textOf(getResources(), DB.ACTIVITY.SPORT_RUNNING)); } else { - int sport = Sport.valueOf(ai.getSport()).getDbValue(); viewHolder.tvSport.setText(Sport.textOf(getResources(), sport)); }