diff --git a/src/androidTest/java/de/dennisguse/opentracks/data/models/ActivityTypeTest.java b/src/androidTest/java/de/dennisguse/opentracks/data/models/ActivityTypeTest.java new file mode 100644 index 000000000..4dd0ff2b8 --- /dev/null +++ b/src/androidTest/java/de/dennisguse/opentracks/data/models/ActivityTypeTest.java @@ -0,0 +1,78 @@ +package de.dennisguse.opentracks.data.models; + +import static org.junit.Assert.assertEquals; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Locale; + +@RunWith(AndroidJUnit4.class) +public class ActivityTypeTest { + + @Test + public void findBy_id() { + assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("trail running")); + } + + @Test + public void findBy_idOfEveryActivityType() { + for (ActivityType activityType : ActivityType.values()) { + assertEquals(activityType, ActivityType.findBy(activityType.getId())); + } + } + + @Test + public void findBy_synonymsOfEveryActivityType() { + for (ActivityType activityType : ActivityType.values()) { + for (String synonym : activityType.synonyms) { + assertEquals(activityType, ActivityType.findBy(synonym)); + assertEquals(activityType, ActivityType.findBy(synonym.toUpperCase(Locale.ROOT).replace(' ', '_'))); + } + } + } + + @Test + public void findBy_ignoresCase() { + assertEquals(ActivityType.RUNNING, ActivityType.findBy("RUNNING")); + assertEquals(ActivityType.ATV, ActivityType.findBy("atv")); + } + + @Test + public void findBy_ignoresSeparators() { + assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUNNING")); + assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("mountain-biking")); + assertEquals(ActivityType.CROSS_COUNTRY_SKIING, ActivityType.findBy("cross_country_skiing")); + } + + @Test + public void findBy_ignoresSurroundingAndRepeatedWhitespace() { + assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy(" Mountain Biking ")); + } + + @Test + public void findBy_synonym() { + assertEquals(ActivityType.RUNNING, ActivityType.findBy("treadmill")); + assertEquals(ActivityType.TRAIL_RUNNING, ActivityType.findBy("TRAIL_RUN")); + assertEquals(ActivityType.MOUNTAIN_BIKING, ActivityType.findBy("MOUNTAIN_BIKE")); + assertEquals(ActivityType.SWIMMING_OPEN, ActivityType.findBy("SWIMMING_OPENWATER")); + assertEquals(ActivityType.BIKING, ActivityType.findBy("bike")); + } + + @Test + public void findBy_unknown() { + assertEquals(ActivityType.UNKNOWN, ActivityType.findBy("hovercraft")); + } + + @Test + public void findBy_null() { + assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(null)); + } + + @Test + public void findBy_blank() { + assertEquals(ActivityType.UNKNOWN, ActivityType.findBy(" ")); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/data/models/ActivityType.java b/src/main/java/de/dennisguse/opentracks/data/models/ActivityType.java index f51bc62de..998695d8c 100644 --- a/src/main/java/de/dennisguse/opentracks/data/models/ActivityType.java +++ b/src/main/java/de/dennisguse/opentracks/data/models/ActivityType.java @@ -7,6 +7,7 @@ import androidx.annotation.NonNull; import java.util.Arrays; import java.util.List; +import java.util.Locale; import de.dennisguse.opentracks.R; @@ -14,13 +15,13 @@ public enum ActivityType { AIRPLANE("airplane", R.string.activity_type_airplane, R.drawable.ic_activity_flight_24dp, true), ATV("ATV", R.string.activity_type_atv, R.drawable.ic_activity_drive_24dp, true), - BIKING("biking", R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true), + BIKING("biking", List.of("bike", "indoor cycling"), R.string.activity_type_biking, R.drawable.ic_activity_bike_24dp, true), BLIMP("blimp", R.string.activity_type_blimp, ActivityIcon.ICON_UNKNOWN, true), BOAT("boat", R.string.activity_type_boat, R.drawable.ic_activity_boat_24dp, true), CLIMBING("climbing", R.string.activity_type_climbing, R.drawable.ic_activity_climbing_24dp, false), COMMERCIAL_AIRPLANE("commercial airplane", R.string.activity_type_commercial_airplane, R.drawable.ic_activity_flight_24dp, true), CROSS_COUNTRY_SKIING("cross-country skiing", R.string.activity_type_cross_country_skiing, R.drawable.ic_activity_skiing_24dp, true), - CYCLING("cycling", R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true), + CYCLING("cycling", List.of("cycle"), R.string.activity_type_cycling, R.drawable.ic_activity_bike_24dp, true), DIRT_BIKE("dirt bike", R.string.activity_type_dirt_bike, R.drawable.ic_activity_mtb_24dp, true), DONKEY_BACK_RIDING("donkey back riding", R.string.activity_type_donkey_back_riding, ActivityIcon.ICON_UNKNOWN, true), DRIVING("driving", R.string.activity_type_driving, R.drawable.ic_activity_drive_24dp, true), @@ -30,21 +31,21 @@ public enum ActivityType { FERRY("ferry", R.string.activity_type_ferry, R.drawable.ic_activity_boat_24dp, true), FRISBEE("frisbee", R.string.activity_type_frisbee, ActivityIcon.ICON_UNKNOWN, true), GLIDING("gliding", R.string.activity_type_gliding, ActivityIcon.ICON_UNKNOWN, true), - GRAVEL_BIKING("gravel biking", R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true), + GRAVEL_BIKING("gravel biking", List.of("gravel bike"), R.string.activity_type_gravel_biking, R.drawable.ic_activity_bike_24dp, true), HANG_GLIDING("hang gliding", R.string.activity_type_hang_gliding, ActivityIcon.ICON_UNKNOWN, true), HELICOPTER("helicopter", R.string.activity_type_helicopter, ActivityIcon.ICON_UNKNOWN, true), - HIKING("hiking", R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false), + HIKING("hiking", List.of("hike"), R.string.activity_type_hiking, R.drawable.ic_activity_walk_24dp, false), HORSE_BACK_RIDING("horse back riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true), HOT_AIR_BALLOON("hot air balloon", R.string.activity_type_hot_air_balloon, ActivityIcon.ICON_UNKNOWN, true), ICE_SAILING("ice sailing", R.string.activity_type_ice_sailing, ActivityIcon.ICON_UNKNOWN, true), - INLINE_SKATING("inline skating", R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true), + INLINE_SKATING("inline skating", List.of("inline skate"), R.string.activity_type_inline_skating, R.drawable.ic_activity_inline_skating_24dp, true), KAYAKING("kayaking", R.string.activity_type_kayaking, R.drawable.ic_activity_kayaking_24dp, true), KITE_SURFING("kite surfing", R.string.activity_type_kite_surfing, ActivityIcon.ICON_UNKNOWN, true), LAND_SAILING("land sailing", R.string.activity_type_land_sailing, ActivityIcon.ICON_UNKNOWN, true), MIXED_TYPE("mixed type", R.string.activity_type_mixed_type, ActivityIcon.ICON_UNKNOWN, true), MOTOR_BIKE("motor bike", R.string.activity_type_motor_bike, R.drawable.ic_activity_motorbike_24dp, true), MOTOR_BOATING("motor boating", R.string.activity_type_motor_boating, R.drawable.ic_activity_boat_24dp, true), - MOUNTAIN_BIKING("mountain biking", R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true), + MOUNTAIN_BIKING("mountain biking", List.of("mountain bike"), R.string.activity_type_mountain_biking, R.drawable.ic_activity_mtb_24dp, true), OFF_TRAIL_HIKING("off trail hiking", R.string.activity_type_off_trail_hiking, R.drawable.ic_activity_walk_24dp, false), OTHER("other", R.string.activity_type_other, ActivityIcon.ICON_UNKNOWN, true), PADDLING("paddling", R.string.activity_type_paddling, ActivityIcon.ICON_UNKNOWN, true), @@ -53,46 +54,57 @@ public enum ActivityType { RC_BOAT("RC boat", R.string.activity_type_rc_boat, R.drawable.ic_activity_boat_24dp, true), RC_HELICOPTER("RC helicopter", R.string.activity_type_rc_helicopter, ActivityIcon.ICON_UNKNOWN, true), RIDING("riding", R.string.activity_type_horse_back_riding, ActivityIcon.ICON_UNKNOWN, true), - ROAD_BIKING("road biking", R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true), + ROAD_BIKING("road biking", List.of("road bike"), R.string.activity_type_road_biking, R.drawable.ic_activity_bike_24dp, true), ROLLER_SKIING("roller skiing", R.string.activity_type_roller_skiing, ActivityIcon.ICON_UNKNOWN, true), ROWING("rowing", R.string.activity_type_rowing, ActivityIcon.ICON_UNKNOWN, true), - RUNNING("running", R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false), + RUNNING("running", List.of("run", "treadmill"), R.string.activity_type_running, R.drawable.ic_activity_run_24dp, false), SAILING("sailing", R.string.activity_type_sailing, R.drawable.ic_activity_sailing_24dp, true), KICKSCOOTER("kickscooter", R.string.activity_type_kickscooter, R.drawable.ic_activity_scooter_24dp, true), SEAPLANE("seaplane", R.string.activity_type_seaplane, R.drawable.ic_activity_flight_24dp, true), - SKATE_BOARDING("skateboarding", R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true), + SKATE_BOARDING("skateboarding", List.of("skateboard"), R.string.activity_type_skate_boarding, R.drawable.ic_activity_skateboarding_24dp, true), SKATING("skating", R.string.activity_type_skating, ActivityIcon.ICON_UNKNOWN, true), SKIING("skiing", R.string.activity_type_skiing, R.drawable.ic_activity_skiing_24dp, true), SKY_JUMPING("sky jumping", R.string.activity_type_sky_jumping, ActivityIcon.ICON_UNKNOWN, true), SLED("sled", R.string.activity_type_sled, ActivityIcon.ICON_UNKNOWN, true), - SNOW_BOARDING("snowboarding", R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true), + SNOW_BOARDING("snowboarding", List.of("snowboard"), R.string.activity_type_snow_boarding, R.drawable.ic_activity_snowboarding_24dp, true), SNOW_SHOEING("snow shoeing", R.string.activity_type_snow_shoeing, ActivityIcon.ICON_UNKNOWN, true), SPEED_WALKING("speed walking", R.string.activity_type_speed_walking, R.drawable.ic_activity_walk_24dp, false), STREET_RUNNING("street running", R.string.activity_type_street_running, R.drawable.ic_activity_run_24dp, false), SURFING("surfing", R.string.activity_type_surfing, ActivityIcon.ICON_UNKNOWN, true), TRACK_CYCLING("track cycling", R.string.activity_type_track_cycling, R.drawable.ic_activity_bike_24dp, true), - TRACK_RUNNING("track running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), + TRACK_RUNNING("track running", List.of("track run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), TRAIL_HIKING("trail hiking", R.string.activity_type_trail_hiking, R.drawable.ic_activity_walk_24dp, false), - TRAIL_RUNNING("trail running", R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), + TRAIL_RUNNING("trail running", List.of("trail run"), R.string.activity_type_trail_running, R.drawable.ic_activity_run_24dp, false), TRAIN("train", R.string.activity_type_train, ActivityIcon.ICON_UNKNOWN, true), ULTIMATE_FRISBEE("ultimate frisbee", R.string.activity_type_ultimate_frisbee, ActivityIcon.ICON_UNKNOWN, true), WAKEBOARDING("wakeboarding", R.string.activity_type_wakeboarding, ActivityIcon.ICON_UNKNOWN, true), - WALKING("walking", R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false), + WALKING("walking", List.of("walk"), R.string.activity_type_walking, R.drawable.ic_activity_walk_24dp, false), WATER_SKIING("water skiing", R.string.activity_type_water_skiing, ActivityIcon.ICON_UNKNOWN, true), WIND_SURFING("wind surfing", R.string.activity_type_wind_surfing, ActivityIcon.ICON_UNKNOWN, true), - SWIMMING("swimming", R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false), - SWIMMING_OPEN("swimming in open water", R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false), + SWIMMING("swimming", List.of("swim"), R.string.activity_type_swimming, R.drawable.ic_activity_swimming_24dp, false), + SWIMMING_OPEN("swimming in open water", List.of("swimming openwater", "open water swimming"), R.string.activity_type_swimming_open, R.drawable.ic_activity_swimming_open_24dp, false), WORKOUT("workout", R.string.activity_type_workout, R.drawable.ic_activity_workout_24dp, false), UNKNOWN("unknown", R.string.activity_type_unknown, ActivityIcon.ICON_UNKNOWN, true); final String id; + /** + * Spellings used by other applications that {@link #normalize(String)} cannot map onto {@link #id}. + * Already normalized. + */ + final List synonyms; + final int iconDrawableId; final boolean showSpeedPreferred; final int localizedStringId; ActivityType(String id, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) { + this(id, List.of(), localizedStringId, iconDrawableId, showSpeedPreferred); + } + + ActivityType(String id, List synonyms, int localizedStringId, int iconDrawableId, boolean showSpeedPreferred) { this.id = id; + this.synonyms = synonyms; this.localizedStringId = localizedStringId; this.iconDrawableId = iconDrawableId; this.showSpeedPreferred = showSpeedPreferred; @@ -121,12 +133,39 @@ public enum ActivityType { .toList(); } + /** + * Lowercases and reduces "_", "-" and whitespace runs to a single space. + * Lowercasing is locale-independent: under a Turkish locale the I of "RUNNING" would otherwise + * lowercase to a dotless i and stop matching "running". + */ + @NonNull + private static String normalize(String activityTypeId) { + if (activityTypeId == null) { + return ""; + } + + return activityTypeId.toLowerCase(Locale.ROOT) + .replaceAll("[\\s_-]+", " ") + .trim(); + } + + /** + * Resolves an activity type as written by another application; GPX's type element is free text. + */ @NonNull public static ActivityType findBy(String activityTypeId) { - return Arrays.stream(ActivityType.values()).filter( - it -> it.id.equals(activityTypeId) - ).findFirst() - .orElse(ActivityType.UNKNOWN); + String normalized = normalize(activityTypeId); + if (normalized.isEmpty()) { + return ActivityType.UNKNOWN; + } + + return Arrays.stream(ActivityType.values()) + .filter(it -> normalize(it.id).equals(normalized)) + .findFirst() + .orElseGet(() -> Arrays.stream(ActivityType.values()) + .filter(it -> it.synonyms.contains(normalized)) + .findFirst() + .orElse(ActivityType.UNKNOWN)); } @NonNull