├── app ├── .gitignore ├── aars │ └── antpluginlib_3-8-0.aar ├── src │ ├── main │ │ ├── res │ │ │ ├── font │ │ │ │ └── coxswain.ttf │ │ │ ├── raw │ │ │ │ ├── whistle_long.mp3 │ │ │ │ ├── whistle_double.mp3 │ │ │ │ └── whistle_short.mp3 │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-hdpi │ │ │ │ ├── notification.png │ │ │ │ ├── ic_close_black_24dp.png │ │ │ │ ├── ic_close_white_24dp.png │ │ │ │ ├── ic_share_black_24dp.png │ │ │ │ ├── ic_share_white_24dp.png │ │ │ │ ├── ic_sort_black_24dp.png │ │ │ │ ├── ic_sort_white_24dp.png │ │ │ │ ├── baseline_week_white_24.png │ │ │ │ ├── baseline_month_white_24.png │ │ │ │ ├── baseline_today_white_24.png │ │ │ │ ├── ic_mode_edit_black_24dp.png │ │ │ │ ├── ic_more_vert_black_24dp.png │ │ │ │ ├── ic_more_vert_white_24dp.png │ │ │ │ ├── baseline_bluetooth_black_24.png │ │ │ │ ├── baseline_bluetooth_white_24.png │ │ │ │ ├── baseline_filter_none_black_24.png │ │ │ │ └── baseline_filter_none_white_24.png │ │ │ ├── drawable-mdpi │ │ │ │ ├── notification.png │ │ │ │ ├── ic_close_black_24dp.png │ │ │ │ ├── ic_close_white_24dp.png │ │ │ │ ├── ic_share_black_24dp.png │ │ │ │ ├── ic_share_white_24dp.png │ │ │ │ ├── ic_sort_black_24dp.png │ │ │ │ ├── ic_sort_white_24dp.png │ │ │ │ ├── baseline_week_white_24.png │ │ │ │ ├── baseline_month_white_24.png │ │ │ │ ├── baseline_today_white_24.png │ │ │ │ ├── ic_mode_edit_black_24dp.png │ │ │ │ ├── ic_more_vert_black_24dp.png │ │ │ │ ├── ic_more_vert_white_24dp.png │ │ │ │ ├── baseline_bluetooth_black_24.png │ │ │ │ ├── baseline_bluetooth_white_24.png │ │ │ │ ├── baseline_filter_none_black_24.png │ │ │ │ └── baseline_filter_none_white_24.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── drawable-xhdpi │ │ │ │ ├── notification.png │ │ │ │ ├── ic_sort_black_24dp.png │ │ │ │ ├── ic_sort_white_24dp.png │ │ │ │ ├── ic_close_black_24dp.png │ │ │ │ ├── ic_close_white_24dp.png │ │ │ │ ├── ic_share_black_24dp.png │ │ │ │ ├── ic_share_white_24dp.png │ │ │ │ ├── baseline_month_white_24.png │ │ │ │ ├── baseline_today_white_24.png │ │ │ │ ├── baseline_week_white_24.png │ │ │ │ ├── ic_mode_edit_black_24dp.png │ │ │ │ ├── ic_more_vert_black_24dp.png │ │ │ │ ├── ic_more_vert_white_24dp.png │ │ │ │ ├── baseline_bluetooth_black_24.png │ │ │ │ ├── baseline_bluetooth_white_24.png │ │ │ │ ├── baseline_filter_none_black_24.png │ │ │ │ └── baseline_filter_none_white_24.png │ │ │ ├── drawable-xxhdpi │ │ │ │ ├── notification.png │ │ │ │ ├── ic_close_black_24dp.png │ │ │ │ ├── ic_close_white_24dp.png │ │ │ │ ├── ic_share_black_24dp.png │ │ │ │ ├── ic_share_white_24dp.png │ │ │ │ ├── ic_sort_black_24dp.png │ │ │ │ ├── ic_sort_white_24dp.png │ │ │ │ ├── baseline_week_white_24.png │ │ │ │ ├── baseline_month_white_24.png │ │ │ │ ├── baseline_today_white_24.png │ │ │ │ ├── ic_mode_edit_black_24dp.png │ │ │ │ ├── ic_more_vert_black_24dp.png │ │ │ │ ├── ic_more_vert_white_24dp.png │ │ │ │ ├── baseline_bluetooth_black_24.png │ │ │ │ ├── baseline_bluetooth_white_24.png │ │ │ │ ├── baseline_filter_none_black_24.png │ │ │ │ └── baseline_filter_none_white_24.png │ │ │ ├── drawable-xxxhdpi │ │ │ │ ├── notification.png │ │ │ │ ├── ic_close_black_24dp.png │ │ │ │ ├── ic_close_white_24dp.png │ │ │ │ ├── ic_share_black_24dp.png │ │ │ │ ├── ic_share_white_24dp.png │ │ │ │ ├── ic_sort_black_24dp.png │ │ │ │ ├── ic_sort_white_24dp.png │ │ │ │ ├── baseline_month_white_24.png │ │ │ │ ├── baseline_today_white_24.png │ │ │ │ ├── baseline_week_white_24.png │ │ │ │ ├── ic_mode_edit_black_24dp.png │ │ │ │ ├── ic_more_vert_black_24dp.png │ │ │ │ ├── ic_more_vert_white_24dp.png │ │ │ │ ├── baseline_bluetooth_black_24.png │ │ │ │ ├── baseline_bluetooth_white_24.png │ │ │ │ ├── baseline_filter_none_black_24.png │ │ │ │ └── baseline_filter_none_white_24.png │ │ │ ├── xml │ │ │ │ ├── device_filter.xml │ │ │ │ └── file_provider_paths.xml │ │ │ ├── drawable │ │ │ │ ├── progress.xml │ │ │ │ ├── progress_land.xml │ │ │ │ ├── shadow_left.xml │ │ │ │ ├── shadow_right.xml │ │ │ │ ├── shadow_top.xml │ │ │ │ ├── binding.xml │ │ │ │ └── segment.xml │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── arrays.xml │ │ │ │ └── attrs.xml │ │ │ ├── layout │ │ │ │ ├── layout_values.xml │ │ │ │ ├── layout_values_item.xml │ │ │ │ ├── layout_performance.xml │ │ │ │ ├── layout_programs.xml │ │ │ │ ├── layout_workouts.xml │ │ │ │ ├── layout_appbar.xml │ │ │ │ ├── layout_settings.xml │ │ │ │ ├── layout_binding.xml │ │ │ │ ├── layout_tabs.xml │ │ │ │ ├── layout_performance_item.xml │ │ │ │ ├── layout_segments_item.xml │ │ │ │ ├── layout_bluetooth_devices_item.xml │ │ │ │ ├── layout_program.xml │ │ │ │ ├── layout_workout.xml │ │ │ │ ├── layout_bluetooth.xml │ │ │ │ ├── layout_programs_item.xml │ │ │ │ ├── layout_workouts_item.xml │ │ │ │ └── layout_main.xml │ │ │ ├── values-w820dp │ │ │ │ └── dimens.xml │ │ │ └── menu │ │ │ │ ├── menu_workouts.xml │ │ │ │ ├── menu_performance.xml │ │ │ │ ├── menu_main.xml │ │ │ │ ├── menu_workout_item.xml │ │ │ │ └── menu_programs_item.xml │ │ └── java │ │ │ └── svenmeier │ │ │ └── coxswain │ │ │ ├── io │ │ │ ├── Import.java │ │ │ ├── Calendar.java │ │ │ ├── Program2Json.java │ │ │ ├── ImportIntention.java │ │ │ ├── Export.java │ │ │ ├── Json2Program.java │ │ │ └── ProgramImport.java │ │ │ ├── garmin │ │ │ ├── ICourse.java │ │ │ ├── StationaryCourse.java │ │ │ ├── ShareReceiver.java │ │ │ ├── Course.java │ │ │ ├── TCX2Course.java │ │ │ ├── TcxShareExport.java │ │ │ └── TcxImport.java │ │ │ ├── motivator │ │ │ └── Motivator.java │ │ │ ├── view │ │ │ ├── charts │ │ │ │ ├── TimeValueFormatter.java │ │ │ │ ├── LimitArea.java │ │ │ │ └── XAxisRenderer2.java │ │ │ ├── preference │ │ │ │ ├── ResultPreference.java │ │ │ │ ├── EditTextPreference.java │ │ │ │ └── RingtonePreference.java │ │ │ ├── SegmentsData.java │ │ │ ├── GridScroll.java │ │ │ ├── LevelView.java │ │ │ ├── ExportProgramDialogFragment.java │ │ │ ├── DeleteDialogFragment.java │ │ │ ├── ExportWorkoutDialogFragment.java │ │ │ ├── Utils.java │ │ │ └── ValueBinding.java │ │ │ ├── rower │ │ │ ├── wired │ │ │ │ ├── usb │ │ │ │ │ ├── ITransfer.java │ │ │ │ │ ├── Consumer.java │ │ │ │ │ ├── Permission.java │ │ │ │ │ └── UsbConnector.java │ │ │ │ ├── IProtocol.java │ │ │ │ ├── Field.java │ │ │ │ ├── RatioCalculator.java │ │ │ │ ├── PowerCalculator.java │ │ │ │ └── NumberField.java │ │ │ ├── ITrace.java │ │ │ ├── NullTrace.java │ │ │ ├── Stroke.java │ │ │ ├── Duration.java │ │ │ ├── SpeedAdjuster.java │ │ │ ├── Energy.java │ │ │ ├── EnergyAdjuster.java │ │ │ ├── Distance.java │ │ │ └── FileTrace.java │ │ │ ├── util │ │ │ ├── ByteUtils.java │ │ │ ├── ChartUtils.java │ │ │ ├── PermissionActivity.java │ │ │ └── PermissionBlock.java │ │ │ ├── Event.java │ │ │ ├── gym │ │ │ ├── Difficulty.java │ │ │ └── Snapshot.java │ │ │ ├── GymVersioning.java │ │ │ ├── GymContentProvider.java │ │ │ ├── SettingsActivity.java │ │ │ ├── bluetooth │ │ │ └── Fields.java │ │ │ ├── Heart.java │ │ │ ├── GymLocator.java │ │ │ ├── Coxswain.java │ │ │ ├── CompactService.java │ │ │ └── sensors │ │ │ └── SensorsHeart.java │ ├── androidTest │ │ └── java │ │ │ └── svenmeier │ │ │ └── coxswain │ │ │ └── ApplicationTest.java │ └── test │ │ ├── java │ │ └── svenmeier │ │ │ └── coxswain │ │ │ ├── bluetooth │ │ │ └── GattScannerTest.java │ │ │ ├── rower │ │ │ └── wired │ │ │ │ ├── TestTrace.java │ │ │ │ ├── UsbTransferTest.java │ │ │ │ ├── EnergyAdjusterTest.java │ │ │ │ ├── RatioCalculatorTest.java │ │ │ │ ├── Protocol4Test.java │ │ │ │ └── usb │ │ │ │ └── TestTransfer.java │ │ │ ├── Program2JsonTest.java │ │ │ ├── Json2ProgramTest.java │ │ │ ├── google │ │ │ └── Workout2FitTest.java │ │ │ ├── gym │ │ │ └── WorkoutTest.java │ │ │ └── garmin │ │ │ └── TCX2WorkoutTest.java │ │ └── resources │ │ ├── empty.tcx │ │ └── snapshots.tcx ├── proguard-rules.pro └── build.gradle ├── .navigation └── app │ └── raw │ └── main.nvg.xml ├── doc ├── FTMS_v1.0.pdf ├── D52QGF-ap_4.02.00.zip ├── google-play │ ├── feature.png │ └── hi-res-icon.png ├── Water Rower S4 Serial Protocol.pdf └── Water Rower S4 S5 USB Protocol Iss 1 04.pdf ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .gitignore ├── settings.gradle ├── adb-wifi-connect ├── gradle.properties ├── README.md ├── gradlew.bat └── changelog /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /.navigation/app/raw/main.nvg.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc/FTMS_v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/FTMS_v1.0.pdf -------------------------------------------------------------------------------- /doc/D52QGF-ap_4.02.00.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/D52QGF-ap_4.02.00.zip -------------------------------------------------------------------------------- /doc/google-play/feature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/google-play/feature.png -------------------------------------------------------------------------------- /app/aars/antpluginlib_3-8-0.aar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/aars/antpluginlib_3-8-0.aar -------------------------------------------------------------------------------- /doc/google-play/hi-res-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/google-play/hi-res-icon.png -------------------------------------------------------------------------------- /app/src/main/res/font/coxswain.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/font/coxswain.ttf -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/raw/whistle_long.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/raw/whistle_long.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/whistle_double.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/raw/whistle_double.mp3 -------------------------------------------------------------------------------- /app/src/main/res/raw/whistle_short.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/raw/whistle_short.mp3 -------------------------------------------------------------------------------- /doc/Water Rower S4 Serial Protocol.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/Water Rower S4 Serial Protocol.pdf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/notification.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /doc/Water Rower S4 S5 USB Protocol Iss 1 04.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/doc/Water Rower S4 S5 USB Protocol Iss 1 04.pdf -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/notification.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_close_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_share_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sort_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_sort_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_close_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_share_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_share_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sort_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_sort_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sort_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_sort_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_week_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_week_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_week_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_week_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_share_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_close_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_share_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sort_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_sort_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_close_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_close_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_share_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_share_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sort_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_sort_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_sort_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_month_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_month_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_today_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_today_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_mode_edit_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_more_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_month_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_month_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_today_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_today_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_mode_edit_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_more_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_month_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_month_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_today_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_today_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_week_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_week_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_mode_edit_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_more_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_week_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_week_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/xml/device_filter.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_bluetooth_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_bluetooth_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_bluetooth_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_bluetooth_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_bluetooth_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_bluetooth_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_bluetooth_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_bluetooth_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_month_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_month_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_today_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_today_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_mode_edit_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_more_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_month_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_month_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_today_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_today_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_week_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_week_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_mode_edit_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_mode_edit_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_more_vert_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_more_vert_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_more_vert_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/ic_more_vert_white_24dp.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | /.idea 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | shell.nix 11 | /.android 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_filter_none_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_filter_none_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/baseline_filter_none_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-hdpi/baseline_filter_none_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_filter_none_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_filter_none_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/baseline_filter_none_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-mdpi/baseline_filter_none_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_bluetooth_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_bluetooth_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_bluetooth_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_bluetooth_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_bluetooth_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_bluetooth_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_bluetooth_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_bluetooth_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_filter_none_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_filter_none_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/baseline_filter_none_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xhdpi/baseline_filter_none_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_filter_none_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_filter_none_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/baseline_filter_none_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxhdpi/baseline_filter_none_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_bluetooth_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_bluetooth_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_bluetooth_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_bluetooth_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_filter_none_black_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_filter_none_black_24.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/baseline_filter_none_white_24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svenmeier/coxswain/HEAD/app/src/main/res/drawable-xxxhdpi/baseline_filter_none_white_24.png -------------------------------------------------------------------------------- /app/src/main/res/xml/file_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/Import.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.net.Uri; 4 | 5 | /** 6 | * Created by sven on 27.05.16. 7 | */ 8 | public interface Import { 9 | 10 | void start(Uri uri); 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/progress_land.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Dec 24 09:30:31 CET 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip 7 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/ICourse.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.location.Location; 4 | 5 | public interface ICourse { 6 | 7 | void setDistance(double meters); 8 | 9 | double getLongitude(); 10 | 11 | double getLatitude(); 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 16dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_values.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_values_item.xml: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_performance.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_programs.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_workouts.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_top.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/motivator/Motivator.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.motivator; 2 | 3 | import svenmeier.coxswain.Event; 4 | import svenmeier.coxswain.Gym; 5 | import svenmeier.coxswain.gym.Measurement; 6 | 7 | /** 8 | * Created by sven on 01.10.15. 9 | */ 10 | public interface Motivator { 11 | 12 | void onEvent(Event event, Measurement measurement, Gym.Progress progress); 13 | 14 | void destroy(); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_appbar.xml: -------------------------------------------------------------------------------- 1 | 7 | 11 | -------------------------------------------------------------------------------- /app/src/androidTest/java/svenmeier/coxswain/ApplicationTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.app.Application; 4 | import android.test.ApplicationTestCase; 5 | 6 | /** 7 | * Testing Fundamentals 8 | */ 9 | public class ApplicationTest extends ApplicationTestCase { 10 | public ApplicationTest() { 11 | super(Application.class); 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_workouts.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/charts/TimeValueFormatter.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view.charts; 2 | 3 | import com.github.mikephil.charting.formatter.ValueFormatter; 4 | 5 | public class TimeValueFormatter extends ValueFormatter { 6 | @Override 7 | public String getFormattedValue(float value) { 8 | 9 | int minutes = (int)(value / 60); 10 | int seconds = (int)(value % 60); 11 | 12 | return String.format("%d:%02d", minutes, seconds); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_performance.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | include ':propoid-core' 3 | include ':propoid-db' 4 | include ':propoid-validation' 5 | include ':propoid-ui' 6 | include ':propoid-util' 7 | project(':propoid-core').projectDir = new File('../propoid/propoid-core') 8 | project(':propoid-db').projectDir = new File('../propoid/propoid-db') 9 | project(':propoid-validation').projectDir = new File('../propoid/propoid-validation') 10 | project(':propoid-ui').projectDir = new File('../propoid/propoid-ui') 11 | project(':propoid-util').projectDir = new File('../propoid/propoid-util') 12 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #ff3567ed 4 | #ff1547ad 5 | #FFEEBB00 6 | 7 | #ffffffff 8 | #ff50a9ff 9 | #ffc3a0ff 10 | #ffffa0be 11 | #ffffcc7e 12 | 13 | #338EA8ED 14 | #668EA8ED 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/usb/ITransfer.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired.usb; 2 | 3 | import java.util.Iterator; 4 | 5 | /** 6 | */ 7 | public interface ITransfer { 8 | 9 | int PARITY_NONE = 0; 10 | int PARITY_ODD = 1; 11 | int PARITY_EVEN = 2; 12 | int PARITY_MARK = 3; 13 | int PARITY_SPACE = 4; 14 | 15 | int STOP_BIT_1_0 = 0; 16 | int STOP_BIT_1_5 = 1; 17 | int STOP_BIT_2_0 = 2; 18 | 19 | void setTimeout(int timeout); 20 | 21 | void setBaudrate(int baudRate); 22 | 23 | void setData(int dataBits, int parity, int stopBits, boolean tx); 24 | 25 | void produce(byte[] b); 26 | 27 | Consumer consumer(); 28 | } -------------------------------------------------------------------------------- /adb-wifi-connect: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | ADB=~/opt/android/sdk/platform-tools/adb 4 | LAST=".adb-wifi-connect.last" 5 | 6 | # instruct device to listen on tcpip 7 | $ADB tcpip 5555 8 | 9 | # give device time to aquire port 10 | sleep 2 11 | 12 | # get net configuration 13 | NETCONFIG=$($ADB shell netcfg) 14 | 15 | # match for line starting with ip for wlan0 16 | if [[ $NETCONFIG =~ wlan0[[:alpha:]|[:space:]]*([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+).* ]]; then 17 | IP="${BASH_REMATCH[1]}" 18 | echo "current device on $IP" 19 | echo "$IP" > "$LAST" 20 | else 21 | IP=$(<"$LAST") 22 | echo "last device on $IP" 23 | fi 24 | 25 | # connect to device 26 | $ADB connect $IP 27 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/util/ByteUtils.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.util; 2 | 3 | public class ByteUtils { 4 | public static String toHex(byte[] buffer) { 5 | StringBuilder string = new StringBuilder(buffer.length * 3); 6 | 7 | for (int c = 0; c < buffer.length; c++) { 8 | if (c > 0) { 9 | string.append(' '); 10 | } 11 | 12 | int b = buffer[c] & 0xFF; 13 | 14 | string.append(hex[b >>> 4]); 15 | string.append(hex[b & 0x0F]); 16 | } 17 | 18 | return string.toString(); 19 | } 20 | 21 | private static final char[] hex = "0123456789ABCDEF".toCharArray(); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/bluetooth/GattScannerTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.bluetooth; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.RequiresApi; 6 | 7 | import org.junit.Test; 8 | 9 | import java.util.UUID; 10 | 11 | import static junit.framework.Assert.assertEquals; 12 | 13 | /** 14 | * Test for {@link BlueWriter}. 15 | */ 16 | @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2) 17 | public class GattScannerTest { 18 | 19 | @Test 20 | public void test() { 21 | assertEquals(UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"), BlueWriter.uuid(0x2A37)); 22 | 23 | assertEquals(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"), BlueWriter.uuid(0x2902)); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/StationaryCourse.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.location.Location; 4 | 5 | public class StationaryCourse implements ICourse { 6 | 7 | private Location location; 8 | 9 | public StationaryCourse(Location location) { 10 | if (location == null) { 11 | // Greenwich 12 | location = new Location(""); 13 | location.setLatitude(0); 14 | location.setLongitude(0); 15 | } 16 | 17 | this.location = location; 18 | } 19 | 20 | public void setDistance(double meters) { 21 | } 22 | 23 | public double getLongitude() { 24 | return location.getLongitude(); 25 | } 26 | 27 | public double getLatitude() { 28 | return location.getLatitude(); 29 | } 30 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /home/sven/opt/android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_binding.xml: -------------------------------------------------------------------------------- 1 | 5 | 13 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_tabs.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_performance_item.xml: -------------------------------------------------------------------------------- 1 | 7 | 18 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/TestTrace.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired; 2 | 3 | import svenmeier.coxswain.rower.ITrace; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | */ 9 | class TestTrace implements ITrace { 10 | 11 | public StringBuilder result = new StringBuilder(); 12 | 13 | @Override 14 | public void comment(CharSequence string) { 15 | result.append('#'); 16 | result.append(string); 17 | } 18 | 19 | @Override 20 | public void onOutput(CharSequence string) { 21 | result.append('>'); 22 | result.append(string); 23 | } 24 | 25 | @Override 26 | public void onInput(CharSequence string) { 27 | result.append('<'); 28 | result.append(string); 29 | } 30 | 31 | @Override 32 | public void close() { 33 | 34 | } 35 | 36 | @Override 37 | public String toString() { 38 | return result.toString(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/Event.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain; 17 | 18 | /** 19 | * Created by sven on 15.09.15. 20 | */ 21 | public enum Event { 22 | ACKNOWLEDGED, PROGRAM_START, SEGMENT_CHANGED, PROGRAM_FINISHED, REJECTED 23 | } 24 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/IProtocol.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower.wired; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | public interface IProtocol { 21 | 22 | void reset(); 23 | 24 | void transfer(Measurement measurement); 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/ITrace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | public interface ITrace { 19 | 20 | void comment(CharSequence string); 21 | 22 | void onOutput(CharSequence string); 23 | 24 | void onInput(CharSequence string); 25 | 26 | void close(); 27 | } 28 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/UsbTransferTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired; 2 | 3 | import org.junit.Test; 4 | 5 | import svenmeier.coxswain.rower.wired.usb.ITransfer; 6 | import svenmeier.coxswain.rower.wired.usb.UsbTransfer; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | /** 11 | */ 12 | public class UsbTransferTest { 13 | 14 | @Test 15 | public void baudrate() throws Exception { 16 | assertEquals(0x09c4, UsbTransfer.divisor(1200)); 17 | assertEquals(0x001A, UsbTransfer.divisor(115200)); 18 | } 19 | 20 | @Test 21 | public void data() throws Exception { 22 | assertEquals(0b0_0_000_000_00001000, UsbTransfer.data(8, ITransfer.PARITY_NONE, ITransfer.STOP_BIT_1_0, false)); 23 | assertEquals(0b0_0_001_001_00001000, UsbTransfer.data(8, ITransfer.PARITY_ODD, ITransfer.STOP_BIT_1_5, false)); 24 | assertEquals(0b0_1_010_010_00000100, UsbTransfer.data(4, ITransfer.PARITY_EVEN, ITransfer.STOP_BIT_2_0, true)); 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/ShareReceiver.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.ComponentName; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | 8 | import propoid.util.content.Preference; 9 | import svenmeier.coxswain.R; 10 | 11 | import static android.content.Intent.EXTRA_CHOSEN_COMPONENT; 12 | 13 | /** 14 | * See {@link TcxShareExport}. 15 | */ 16 | public class ShareReceiver extends BroadcastReceiver 17 | { 18 | @Override 19 | public void onReceive(Context context, Intent intent) { 20 | ComponentName componentName = intent.getParcelableExtra(EXTRA_CHOSEN_COMPONENT); 21 | 22 | Preference.getString(context, R.string.preference_export_tcx_share_package).set(componentName.getPackageName()); 23 | } 24 | 25 | public static Intent newIntent(Context context) { 26 | return new Intent(context, ShareReceiver.class); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 10 | 11 | 14 | 15 | 17 | 18 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/test/resources/empty.tcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-06-15T00:00:00.000Z 6 | 7 | 2 8 | 6 9 | 3 10 | Active 11 | Manual 12 | 13 | 14 | 2 15 | 16 | 17 | 18 | Created by Coxswain 19 | 20 | 21 | - 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Waterrower 5 | Sensor 6 | Bluetooth 7 | Ant+ 8 | 9 | 10 | svenmeier.coxswain.Heart 11 | svenmeier.coxswain.sensors.SensorsHeart 12 | svenmeier.coxswain.bluetooth.BluetoothHeart 13 | svenmeier.coxswain.ant.AntHeart 14 | 15 | 16 | 17 | kcal 18 | Wh 19 | kJ 20 | 21 | 22 | 23 | mi 24 | yd 25 | ft 26 | m 27 | 28 | 29 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/preference/ResultPreference.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view.preference; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.util.AttributeSet; 6 | 7 | import androidx.preference.Preference; 8 | 9 | import svenmeier.coxswain.R; 10 | 11 | /** 12 | * A preference that needs a result from an {@link Intent}. 13 | */ 14 | public abstract class ResultPreference extends Preference { 15 | 16 | public ResultPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 17 | super(context, attrs, defStyleAttr, defStyleRes); 18 | } 19 | 20 | public ResultPreference(Context context, AttributeSet attrs, int defStyleAttr) { 21 | super(context, attrs, defStyleAttr); 22 | } 23 | 24 | public ResultPreference(Context context, AttributeSet attrs) { 25 | super(context, attrs); 26 | } 27 | 28 | public ResultPreference(Context context) { 29 | super(context); 30 | } 31 | 32 | public abstract Intent getRequest(); 33 | 34 | public abstract void onResult(Intent intent); 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_workout_item.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/NullTrace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | public class NullTrace implements ITrace { 19 | 20 | @Override 21 | public void comment(CharSequence string) { 22 | 23 | } 24 | 25 | @Override 26 | public void onOutput(CharSequence string) { 27 | 28 | } 29 | 30 | @Override 31 | public void onInput(CharSequence string) { 32 | 33 | } 34 | 35 | @Override 36 | public void close() { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/progress/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default toUnit: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/progress/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # ./gradlew githubRelease 21 | coxswain-versionCode=88 22 | coxswain-versionName=8.8 23 | 24 | coxswain-private=/home/sven/Documents/coxswain/coxswain-private.gradle 25 | 26 | android.useAndroidX=true 27 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/usb/Consumer.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired.usb; 2 | 3 | import android.hardware.usb.UsbDeviceConnection; 4 | import android.hardware.usb.UsbEndpoint; 5 | 6 | /** 7 | */ 8 | public abstract class Consumer { 9 | 10 | int index = 0; 11 | 12 | protected abstract byte[] getBuffer(); 13 | 14 | protected abstract int getBufferLength(); 15 | 16 | protected abstract void setBufferLength(int bufferLength); 17 | 18 | public boolean hasNext() { 19 | return index < getBufferLength(); 20 | } 21 | 22 | public byte next() { 23 | if (index == getBufferLength()) { 24 | throw new IndexOutOfBoundsException(); 25 | } 26 | byte b = getBuffer()[index]; 27 | index++; 28 | return b; 29 | } 30 | 31 | public byte[] consumed() { 32 | byte[] consumed = new byte[index]; 33 | System.arraycopy(getBuffer(), 0, consumed, 0, index); 34 | 35 | int bufferLength = getBufferLength(); 36 | bufferLength -= index; 37 | System.arraycopy(getBuffer(), index, getBuffer(), 0, bufferLength); 38 | setBufferLength(bufferLength); 39 | 40 | index = 0; 41 | 42 | return consumed; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/binding.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_segments_item.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 20 | 21 | 27 | 28 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_programs_item.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/gym/Difficulty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.gym; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | /** 22 | * Created by sven on 07.07.15. 23 | */ 24 | public enum Difficulty { 25 | NONE, REST, EASY, MEDIUM, HARD, PEAK; 26 | 27 | public Difficulty increase() { 28 | Difficulty[] values = values(); 29 | 30 | int next = ordinal() + 1; 31 | if (next == 6) { 32 | // never NONE 33 | next = 1; 34 | } 35 | 36 | return values[next]; 37 | } 38 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/GymVersioning.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.database.Cursor; 4 | import android.database.SQLException; 5 | import android.database.sqlite.SQLiteDatabase; 6 | 7 | import propoid.db.SQL; 8 | import propoid.db.Setting; 9 | import propoid.db.Versioning; 10 | import propoid.db.version.DefaultVersioning; 11 | import propoid.db.version.Upgrade; 12 | 13 | /** 14 | */ 15 | class GymVersioning extends DefaultVersioning { 16 | 17 | GymVersioning() { 18 | add(new WrongIndices()); 19 | } 20 | 21 | /** 22 | * Index names where bogus, thus they were recreated on each start :/. 23 | * Let's drop them all. 24 | */ 25 | private class WrongIndices implements Upgrade { 26 | @Override 27 | public void apply(SQLiteDatabase database) { 28 | Cursor indices = database.rawQuery("SELECT name FROM sqlite_master WHERE type = 'index'", new String[0]); 29 | try { 30 | while (indices.moveToNext()) { 31 | String name = indices.getString(0); 32 | 33 | SQL drop = new SQL(); 34 | drop.raw("DROP INDEX "); 35 | drop.escaped(name); 36 | database.execSQL(drop.toString()); 37 | } 38 | } finally { 39 | indices.close(); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_bluetooth_devices_item.xml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 16 | 17 | 23 | 24 | 29 | 30 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/GymContentProvider.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.content.ContentProvider; 4 | import android.content.ContentValues; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | /** 12 | * Created by sven on 01.11.17. 13 | */ 14 | public class GymContentProvider extends ContentProvider { 15 | @Override 16 | public boolean onCreate() { 17 | return true; 18 | } 19 | 20 | @Nullable 21 | @Override 22 | public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) { 23 | return null; 24 | } 25 | 26 | @Nullable 27 | @Override 28 | public String getType(@NonNull Uri uri) { 29 | return null; 30 | } 31 | 32 | @Nullable 33 | @Override 34 | public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) { 35 | return null; 36 | } 37 | 38 | @Override 39 | public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) { 40 | return 0; 41 | } 42 | 43 | @Override 44 | public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) { 45 | return 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/util/ChartUtils.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.util; 2 | 3 | import android.content.Context; 4 | import android.content.res.TypedArray; 5 | import android.util.TypedValue; 6 | 7 | import com.github.mikephil.charting.charts.BarChart; 8 | import com.github.mikephil.charting.charts.BarLineChartBase; 9 | import com.github.mikephil.charting.charts.Chart; 10 | import svenmeier.coxswain.R; 11 | 12 | 13 | public class ChartUtils { 14 | public static void setTextColor(Context context, Chart chartView) { 15 | 16 | TypedValue typedValue = new TypedValue(); 17 | 18 | TypedArray a = context.obtainStyledAttributes(typedValue.data, new int[] { R.attr.editTextColor }); 19 | int color = a.getColor(0, 0); 20 | a.recycle(); 21 | 22 | if (chartView instanceof BarChart) { 23 | ((BarChart)chartView).getAxisLeft().setTextColor(color); 24 | ((BarChart)chartView).getAxisRight().setTextColor(color); 25 | } 26 | if (chartView instanceof BarLineChartBase) { 27 | ((BarLineChartBase)chartView).getAxisLeft().setTextColor(color); 28 | ((BarLineChartBase)chartView).getAxisRight().setTextColor(color); 29 | } 30 | 31 | chartView.getXAxis().setTextColor(color); 32 | chartView.getLegend().setTextColor(color); 33 | chartView.getDescription().setTextColor(color); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/Program2JsonTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.robolectric.RobolectricTestRunner; 6 | import org.robolectric.annotation.Config; 7 | 8 | import java.io.IOException; 9 | import java.io.StringWriter; 10 | import java.io.Writer; 11 | 12 | import svenmeier.coxswain.gym.Difficulty; 13 | import svenmeier.coxswain.gym.Program; 14 | import svenmeier.coxswain.gym.Segment; 15 | import svenmeier.coxswain.io.Program2Json; 16 | 17 | import static junit.framework.Assert.assertEquals; 18 | 19 | /** 20 | */ 21 | @RunWith(RobolectricTestRunner.class) 22 | @Config(constants = svenmeier.coxswain.BuildConfig.class) 23 | public class Program2JsonTest { 24 | 25 | @Test 26 | public void test() throws IOException { 27 | 28 | Program program = new Program("Test"); 29 | 30 | Segment segment1 = new Segment(Difficulty.HARD); 31 | segment1.setDuration(60); 32 | program.addSegment(segment1); 33 | 34 | Writer writer = new StringWriter(); 35 | 36 | new Program2Json(writer).document(program); 37 | 38 | String actual = writer.toString().replaceAll("[\\s]", ""); 39 | 40 | assertEquals("{\"name\":\"Test\",\"segments\":[{\"difficulty\":\"EASY\",\"distance\":1000},{\"difficulty\":\"HARD\",\"duration\":60}]}", actual); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/Stroke.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import android.content.Context; 19 | 20 | import propoid.util.content.Preference; 21 | import svenmeier.coxswain.R; 22 | 23 | /** 24 | */ 25 | public class Stroke { 26 | 27 | private Context context; 28 | 29 | private int count; 30 | 31 | public String formatted() { 32 | return String.format(context.getString(R.string.strokes_count), count); 33 | } 34 | 35 | public static Stroke count(Context context, int count) { 36 | Stroke stroke = new Stroke(); 37 | 38 | stroke.context = context; 39 | stroke.count = count; 40 | 41 | return stroke; 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_program.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 15 | 22 | 23 | 24 | 25 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/Duration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import android.content.Context; 19 | 20 | import propoid.util.content.Preference; 21 | import svenmeier.coxswain.R; 22 | 23 | import static java.util.concurrent.TimeUnit.SECONDS; 24 | 25 | /** 26 | */ 27 | public class Duration { 28 | 29 | private int seconds; 30 | 31 | public String formatted() { 32 | return String.format("%d:%02d:%02d", SECONDS.toHours(seconds), SECONDS.toMinutes(seconds) % 60, seconds % 60); 33 | } 34 | 35 | public static Duration seconds(Context context, int seconds) { 36 | Duration duration = new Duration(); 37 | 38 | duration.seconds = seconds; 39 | 40 | return duration; 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/EnergyAdjusterTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired; 2 | 3 | import org.junit.Test; 4 | 5 | import svenmeier.coxswain.gym.Measurement; 6 | import svenmeier.coxswain.rower.EnergyAdjuster; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | 10 | /** 11 | * Test for {@link EnergyAdjuster}. 12 | */ 13 | public class EnergyAdjusterTest { 14 | 15 | private void assertAdjusted(int out, int weight, int in) { 16 | Measurement measurement = new Measurement(); 17 | 18 | // needs distance to adjust 19 | measurement.setDistance(1); 20 | 21 | assertEquals(out, new EnergyAdjuster(weight).adjust(measurement, in)); 22 | } 23 | 24 | @Test 25 | public void test() { 26 | assertAdjusted(0, 68, 0); 27 | } 28 | 29 | @Test 30 | public void test26minutes() { 31 | assertAdjusted(261, 65, 273); 32 | assertAdjusted(299, 75, 273); 33 | assertAdjusted(337, 85, 273); 34 | 35 | assertAdjusted(355, 90, 273); 36 | 37 | assertAdjusted(374, 95, 273); 38 | assertAdjusted(412, 105, 273); 39 | assertAdjusted(450, 115, 273); 40 | assertAdjusted(488, 125, 273); 41 | } 42 | 43 | @Test 44 | public void test40minutes() { 45 | assertAdjusted(408, 65, 420); 46 | assertAdjusted(484, 85, 420); 47 | 48 | assertAdjusted(502, 90, 420); 49 | 50 | assertAdjusted(521, 95, 420); 51 | assertAdjusted(559, 105, 420); 52 | assertAdjusted(635, 125, 420); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/Calendar.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.content.ContentResolver; 4 | import android.content.ContentValues; 5 | import android.content.Context; 6 | import android.database.Cursor; 7 | import android.net.Uri; 8 | import android.provider.CalendarContract; 9 | 10 | /** 11 | * Works with calendars. 12 | */ 13 | public class Calendar { 14 | 15 | private Context context; 16 | 17 | public Calendar(Context context) { 18 | this.context = context; 19 | } 20 | 21 | /** 22 | * Get the id of the default calendar. 23 | */ 24 | public int getDefaultId() { 25 | ContentResolver resolver = context.getContentResolver(); 26 | 27 | String[] projection = {CalendarContract.Calendars._ID, CalendarContract.Calendars.IS_PRIMARY}; 28 | String selection = CalendarContract.Calendars.IS_PRIMARY + " = 1"; 29 | 30 | Cursor cursor = resolver.query(CalendarContract.Calendars.CONTENT_URI, projection, selection, null, null); 31 | try { 32 | if (cursor.moveToNext()) { 33 | return cursor.getInt(cursor.getColumnIndex(CalendarContract.Calendars._ID)); 34 | } 35 | } finally { 36 | cursor.close(); 37 | } 38 | 39 | throw new RuntimeException("no default calendar"); 40 | } 41 | 42 | /** 43 | * Insert an event. 44 | * 45 | * @param event 46 | */ 47 | public void insert(ContentValues event) { 48 | ContentResolver content = context.getContentResolver(); 49 | 50 | content.insert(CalendarContract.Events.CONTENT_URI, event); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/charts/LimitArea.java: -------------------------------------------------------------------------------- 1 | 2 | package svenmeier.coxswain.view.charts; 3 | 4 | import android.graphics.Color; 5 | import android.graphics.DashPathEffect; 6 | import android.graphics.Paint; 7 | import android.graphics.Typeface; 8 | 9 | import com.github.mikephil.charting.components.LimitLine; 10 | import com.github.mikephil.charting.utils.Utils; 11 | 12 | /** 13 | * The limit area is an additional feature for all Line-, Bar- and 14 | * ScatterCharts. It allows the displaying of an additional area in the chart 15 | * that marks a certain minium to maximum on the specified axis (x- or y-axis). 16 | * 17 | * @author Philipp Jahoda 18 | */ 19 | public class LimitArea extends LimitLine { 20 | 21 | /** minimum (the y-value or xIndex) */ 22 | private float mMinimum = 0f; 23 | 24 | /** 25 | * Constructor with limit. 26 | */ 27 | public LimitArea(float minimum, float maximum) { 28 | super(maximum); 29 | 30 | mMinimum = minimum; 31 | } 32 | 33 | /** 34 | * Constructor with limit and label. 35 | * 36 | * @param label - provide "" if no label is required 37 | */ 38 | public LimitArea(float minimum, float maximum, String label) { 39 | super(maximum, label); 40 | 41 | mMinimum = mMinimum; 42 | } 43 | 44 | /** 45 | * Returns the minimum that is set for this line. 46 | * 47 | * @return 48 | */ 49 | public float getMinimum() { 50 | return mMinimum; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/Json2ProgramTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.robolectric.RobolectricTestRunner; 6 | import org.robolectric.annotation.Config; 7 | 8 | import java.io.IOException; 9 | import java.io.Reader; 10 | import java.io.StringReader; 11 | 12 | import svenmeier.coxswain.gym.Difficulty; 13 | import svenmeier.coxswain.gym.Program; 14 | import svenmeier.coxswain.gym.Segment; 15 | import svenmeier.coxswain.io.Json2Program; 16 | 17 | import static junit.framework.Assert.assertEquals; 18 | 19 | /** 20 | */ 21 | @RunWith(RobolectricTestRunner.class) 22 | @Config(constants = svenmeier.coxswain.BuildConfig.class) 23 | public class Json2ProgramTest { 24 | 25 | @Test 26 | public void test() throws IOException { 27 | 28 | Reader reader = new StringReader("{\"name\":\"Test\",\"segments\":[{\"difficulty\":\"EASY\",\"distance\":1000},{\"difficulty\":\"HARD\",\"duration\":60}]}"); 29 | 30 | Program program = new Json2Program(reader).program(); 31 | 32 | assertEquals("Test", program.name.get()); 33 | assertEquals(2, program.segments.get().size()); 34 | 35 | Segment segment0 = program.segments.get().get(0); 36 | assertEquals(Difficulty.EASY, segment0.difficulty.get()); 37 | assertEquals(Integer.valueOf(1000), segment0.distance.get()); 38 | 39 | Segment segment1 = program.segments.get().get(1); 40 | assertEquals(Difficulty.HARD, segment1.difficulty.get()); 41 | assertEquals(Integer.valueOf(60), segment1.duration.get()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/Field.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower.wired; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | /** 21 | */ 22 | public class Field { 23 | 24 | public String request; 25 | 26 | public String response; 27 | 28 | protected Field() { 29 | } 30 | 31 | protected Field(String request, String response) { 32 | this.request = request; 33 | this.response = response; 34 | } 35 | 36 | protected boolean input(String message, Measurement measurement) { 37 | if (this.response != null && message.startsWith(response)) { 38 | onInput(message, measurement); 39 | 40 | return true; 41 | } 42 | 43 | return false; 44 | } 45 | 46 | protected void onInput(String message, Measurement measurement) { 47 | 48 | } 49 | 50 | protected void onAfterOutput() { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/Course.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.location.Location; 4 | 5 | import java.util.List; 6 | 7 | public class Course implements ICourse { 8 | 9 | public final List trackpoints; 10 | 11 | private double longitude; 12 | 13 | private double latitude; 14 | 15 | public Course(List trackpoints) { 16 | this.trackpoints = trackpoints; 17 | } 18 | 19 | @Override 20 | public void setDistance(double meters) { 21 | 22 | Trackpoint temp = null; 23 | 24 | for (Trackpoint trackpoint : trackpoints) { 25 | if (temp != null && trackpoint.distanceMeters > meters) { 26 | double factor = (meters - temp.distanceMeters) / (trackpoint.distanceMeters - temp.distanceMeters); 27 | 28 | longitude = temp.location.getLongitude() + (trackpoint.location.getLongitude() - temp.location.getLongitude()) * factor; 29 | latitude = temp.location.getLatitude() + (trackpoint.location.getLatitude() - temp.location.getLatitude()) * factor; 30 | break; 31 | } 32 | 33 | temp = trackpoint; 34 | } 35 | } 36 | 37 | @Override 38 | public double getLongitude() { 39 | return longitude; 40 | } 41 | 42 | @Override 43 | public double getLatitude() { 44 | return latitude; 45 | } 46 | 47 | public static class Trackpoint { 48 | 49 | public final Location location; 50 | 51 | public final double distanceMeters; 52 | 53 | public Trackpoint(Location location, double distanceMeters) { 54 | this.location = location; 55 | this.distanceMeters = distanceMeters; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_workout.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 20 | 27 | 33 | 34 | 35 | 41 | 42 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_bluetooth.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 14 | 15 | 24 | 25 | 31 | 32 | 38 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/SegmentsData.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.view; 17 | 18 | import svenmeier.coxswain.gym.Program; 19 | import svenmeier.coxswain.gym.Segment; 20 | 21 | public class SegmentsData implements SegmentsView.Data { 22 | 23 | private final Program program; 24 | 25 | public SegmentsData(Program program) { 26 | this.program = program; 27 | } 28 | 29 | @Override 30 | public int length() { 31 | return program == null ? 0 : program.getSegmentsCount(); 32 | } 33 | 34 | @Override 35 | public float value(int index) { 36 | return program.getSegment(index).asDuration(); 37 | } 38 | 39 | @Override 40 | public float total() { 41 | float value = 0; 42 | for (Segment segment : program.segments.get()) { 43 | value += segment.asDuration(); 44 | } 45 | return value; 46 | } 47 | 48 | @Override 49 | public int level(int index) { 50 | return program.getSegment(index).difficulty.get().ordinal(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Copyright 2015 Sven Meier 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package svenmeier.coxswain; 18 | 19 | import android.content.Context; 20 | import android.content.Intent; 21 | import android.os.Bundle; 22 | import androidx.fragment.app.FragmentTransaction; 23 | 24 | import svenmeier.coxswain.view.SettingsFragment; 25 | 26 | 27 | public class SettingsActivity extends AbstractActivity { 28 | 29 | protected void onCreate(Bundle savedInstanceState) { 30 | super.onCreate(savedInstanceState); 31 | 32 | setContentView(R.layout.layout_settings); 33 | 34 | // must add fragment via transaction, otherwise is can not be replaced 35 | FragmentTransaction transaction = getSupportFragmentManager().beginTransaction(); 36 | transaction.replace(R.id.settings_fragment, new SettingsFragment(), "settings"); 37 | transaction.commit(); 38 | } 39 | 40 | public static Intent createIntent(Context context) { 41 | Intent intent = new Intent(context, SettingsActivity.class); 42 | 43 | return intent; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Feature](/doc/google-play/feature.png?raw) 2 | 3 | Coxswain helps you organize your rowing: Connect your Android device to your Waterrower and choose your training program. 4 | 5 | - record your workout while watching Youtube videos or other media (choose an intent to be executed on training start) 6 | - row against previous workouts 7 | - adjust the workout display (long press to choice from distance, duration, strokes, energy, speed, pulse, stroke rate, stroke ratio, time, split, average split, delta distance, delta duration) 8 | - export your workout to TCX (Training Center XML) to import it into your favorite fitness tracking App or service. 9 | - export your workout to your calendar 10 | - export your workout to Google Fit (experimental) 11 | - read heart rate from your Waterrower, Android sensor or a connected Bluetooth LE/Ant+ device 12 | 13 | Your Waterrower S4 can be connected to your Android device 14 | - either via USB-OTG adapter (USB-A-female to USB-micro/USB-C resp.), requires On-The-Go (OTG) support on your device 15 | - or Bluetooth, requires the Waterrower S4 Bluetooth Comm Module 16 | 17 | Sideload from https://github.com/svenmeier/coxswain/releases/latest or install from Google Play: 18 | 19 | [Get it on Google Play](http://play.google.com/store/apps/details?id=svenmeier.coxswain) 20 | 21 | This app is under development. Please report problems and ideas and/or support the development by a donation: 22 | 23 | [Donate](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CC3QC76CKCCRY) 24 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/GridScroll.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.view; 17 | 18 | import android.view.View; 19 | 20 | import androidx.recyclerview.widget.LinearLayoutManager; 21 | import androidx.recyclerview.widget.RecyclerView; 22 | 23 | /** 24 | */ 25 | public class GridScroll extends RecyclerView.OnScrollListener { 26 | 27 | @Override 28 | public void onScrollStateChanged(RecyclerView recyclerView, int scrollState) { 29 | if (scrollState == RecyclerView.SCROLL_STATE_IDLE) { 30 | View child = recyclerView.getChildAt(0); 31 | 32 | int top = child.getTop(); 33 | int height = child.getHeight(); 34 | 35 | LinearLayoutManager layoutManager = ((LinearLayoutManager)recyclerView.getLayoutManager()); 36 | int firstVisiblePosition = layoutManager.findFirstVisibleItemPosition(); 37 | 38 | if (top < -height / 2) { 39 | recyclerView.smoothScrollToPosition(firstVisiblePosition + 1); 40 | } else { 41 | recyclerView.smoothScrollToPosition(firstVisiblePosition); 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/bluetooth/Fields.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.bluetooth; 2 | 3 | import android.annotation.TargetApi; 4 | import android.bluetooth.BluetoothGattCharacteristic; 5 | import android.os.Build; 6 | 7 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) 8 | public class Fields { 9 | 10 | public static final int UINT8 = BluetoothGattCharacteristic.FORMAT_UINT8; 11 | 12 | public static final int UINT16 = BluetoothGattCharacteristic.FORMAT_UINT16; 13 | 14 | public static final int UINT32 = BluetoothGattCharacteristic.FORMAT_UINT32; 15 | 16 | public static final int SINT16 = BluetoothGattCharacteristic.FORMAT_SINT16; 17 | 18 | private final BluetoothGattCharacteristic characteristic; 19 | 20 | private int flag; 21 | 22 | private int offset = 0; 23 | 24 | public Fields(BluetoothGattCharacteristic characteristic, int flagSize) { 25 | this.characteristic = characteristic; 26 | 27 | flag = get(flagSize); 28 | } 29 | 30 | public boolean isSet(int bit) { 31 | return (flag & (1 << bit)) != 0; 32 | } 33 | 34 | public boolean isNotSet(int bit) { 35 | return (flag & (1 << bit)) == 0; 36 | } 37 | 38 | public int offset() { 39 | return offset; 40 | } 41 | 42 | public void skip(int format) { 43 | offset += size(format); 44 | } 45 | 46 | public int get(int format) { 47 | int value = characteristic.getIntValue(format, offset); 48 | 49 | offset += size(format); 50 | 51 | return value; 52 | } 53 | 54 | private int size(int format) { 55 | return format & 0xf; 56 | } 57 | 58 | public int[] remaining(int format) { 59 | int count = (characteristic.getValue().length - offset) / size(format); 60 | 61 | int[] values = new int[count]; 62 | for (int c = 0; c < count; c++) { 63 | values[c] = get(format); 64 | } 65 | 66 | return values; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/Heart.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.content.Context; 4 | import android.os.Handler; 5 | import android.util.Log; 6 | 7 | import java.lang.reflect.Constructor; 8 | 9 | import propoid.util.content.Preference; 10 | import svenmeier.coxswain.gym.Measurement; 11 | import svenmeier.coxswain.rower.Rower; 12 | 13 | /** 14 | */ 15 | public class Heart { 16 | 17 | private final Handler handler = new Handler(); 18 | 19 | protected final Context context; 20 | 21 | private final Measurement measurement; 22 | 23 | protected final Callback callback; 24 | 25 | public Heart(Context context, Measurement measurement, Callback callback) { 26 | this.context = context; 27 | this.measurement = measurement; 28 | this.callback = callback; 29 | } 30 | 31 | public void destroy() { 32 | } 33 | 34 | protected void onHeartRate(int heartRate) { 35 | measurement.setPulse(heartRate); 36 | 37 | handler.post(new Runnable() { 38 | @Override 39 | public void run() { 40 | callback.onMeasurement(measurement); 41 | } 42 | }); 43 | } 44 | 45 | public static Heart create(Context context, Measurement measurement, Callback callback) { 46 | Preference sensors = Preference.getString(context, R.string.preference_hardware_heart_sensor); 47 | 48 | String name = sensors.get(); 49 | try { 50 | Constructor constructor = Class.forName(name).getConstructor(Context.class, Measurement.class, Callback.class); 51 | return (Heart) constructor.newInstance(context, measurement, callback); 52 | } catch (Exception ex) { 53 | Log.e(Coxswain.TAG, "cannot create sensor " + name); 54 | return new Heart(context, measurement, callback); 55 | } 56 | } 57 | 58 | public interface Callback { 59 | void onMeasurement(Measurement measurement); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/usb/Permission.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired.usb; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.hardware.usb.UsbDevice; 9 | import android.hardware.usb.UsbManager; 10 | 11 | /** 12 | * Request permission to use a {@link UsbDevice}. 13 | *

14 | * Not used currently, since we restart an activity for intent USB_DEVICE_ATTACHED and 15 | * Android will ask the user for permissions automatically. 16 | */ 17 | public class Permission extends BroadcastReceiver { 18 | 19 | private static final String ACTION_USB_PERMISSION = "svenmeier.coxswain.USB_PERMISSION"; 20 | 21 | private Context context; 22 | 23 | public Permission(Context context) { 24 | this.context = context; 25 | 26 | IntentFilter filter = new IntentFilter(); 27 | filter.addAction(ACTION_USB_PERMISSION); 28 | filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED); 29 | context.registerReceiver(this, filter); 30 | } 31 | 32 | public void destroy() { 33 | context.unregisterReceiver(this); 34 | context = null; 35 | } 36 | 37 | public void request(UsbDevice device) { 38 | UsbManager manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 39 | 40 | manager.requestPermission(device, PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0)); 41 | } 42 | 43 | public void onReceive(Context context, Intent intent) { 44 | String action = intent.getAction(); 45 | 46 | if (ACTION_USB_PERMISSION.equals(action)) { 47 | onRequested(intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)); 48 | } 49 | } 50 | 51 | protected void onRequested(boolean granted) { 52 | } 53 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/segment.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 16 | 18 | 19 | 20 | 21 | 22 | 26 | 28 | 29 | 30 | 31 | 32 | 36 | 38 | 39 | 40 | 41 | 42 | 46 | 48 | 49 | 50 | 51 | 52 | 56 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/LevelView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.view; 17 | 18 | import android.content.Context; 19 | import android.graphics.Canvas; 20 | import android.graphics.drawable.Drawable; 21 | import android.util.AttributeSet; 22 | import android.view.View; 23 | 24 | /** 25 | */ 26 | public class LevelView extends View { 27 | 28 | private int level = 0; 29 | 30 | public LevelView(Context context, AttributeSet attrs) { 31 | super(context, attrs); 32 | } 33 | 34 | public LevelView(Context context, AttributeSet attrs, int defStyle) { 35 | super(context, attrs, defStyle); 36 | } 37 | 38 | public void setLevel(int level) { 39 | if (this.level != level) { 40 | this.level = level; 41 | 42 | invalidate(); 43 | } 44 | } 45 | 46 | @Override 47 | protected int getSuggestedMinimumHeight() { 48 | return super.getSuggestedMinimumHeight(); 49 | } 50 | 51 | @Override 52 | protected void onDraw(Canvas canvas) { 53 | 54 | Drawable background = getBackground(); 55 | if (background != null) { 56 | background.setLevel(level); 57 | } 58 | 59 | super.onDraw(canvas); 60 | } 61 | } -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/RatioCalculatorTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired; 2 | 3 | import org.junit.Test; 4 | 5 | import svenmeier.coxswain.gym.Measurement; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | /** 10 | * Test for {@link RatioCalculator}. 11 | */ 12 | public class RatioCalculatorTest { 13 | 14 | Measurement measurement = new Measurement(); 15 | 16 | @Test 17 | public void test() { 18 | RatioCalculator calculator = new RatioCalculator(); 19 | 20 | long now = 100000; 21 | calculator.strokeEnd(measurement, now); 22 | calculator.strokeEnd(measurement, now + 200); 23 | 24 | now += 1000; 25 | calculator.strokeStart(measurement, now); 26 | calculator.strokeStart(measurement, now + 200); 27 | 28 | now += 1000; 29 | calculator.strokeEnd(measurement, now); 30 | calculator.strokeEnd(measurement, now + 200); 31 | 32 | assertEquals(8, measurement.getStrokeRatio()); 33 | 34 | now += 2000; 35 | calculator.strokeStart(measurement, now); 36 | calculator.strokeStart(measurement, now + 200); 37 | 38 | now += 1000; 39 | calculator.strokeEnd(measurement, now); 40 | calculator.strokeEnd(measurement, now + 200); 41 | 42 | assertEquals(16, measurement.getStrokeRatio()); 43 | 44 | now += 1500; 45 | calculator.strokeStart(measurement, now); 46 | calculator.strokeStart(measurement, now + 200); 47 | 48 | now += 1000; 49 | calculator.strokeEnd(measurement, now); 50 | calculator.strokeEnd(measurement, now + 500); 51 | 52 | assertEquals(12, measurement.getStrokeRatio()); 53 | 54 | now += 500; 55 | calculator.strokeStart(measurement, now); 56 | calculator.strokeStart(measurement, now + 200); 57 | 58 | now += 1000; 59 | calculator.strokeEnd(measurement, now); 60 | calculator.strokeEnd(measurement, now + 200); 61 | 62 | assertEquals(4, measurement.getStrokeRatio()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/ExportProgramDialogFragment.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | 8 | import androidx.fragment.app.DialogFragment; 9 | 10 | import propoid.db.Reference; 11 | import svenmeier.coxswain.Gym; 12 | import svenmeier.coxswain.R; 13 | import svenmeier.coxswain.gym.Program; 14 | import svenmeier.coxswain.io.Export; 15 | import svenmeier.coxswain.io.ProgramExport; 16 | 17 | public class ExportProgramDialogFragment extends DialogFragment { 18 | 19 | private Export export; 20 | 21 | @Override 22 | public Dialog onCreateDialog(Bundle savedInstanceState) { 23 | final Program program = Gym.instance(getActivity()).get(Reference.from(getArguments())); 24 | 25 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 26 | 27 | String[] exports = new String[]{getString(R.string.program_export), getString(R.string.program_export_share)}; 28 | 29 | builder.setTitle(R.string.action_export) 30 | .setItems(exports, new DialogInterface.OnClickListener() { 31 | public void onClick(DialogInterface dialog, int which) { 32 | switch (which) { 33 | case 0: 34 | export = new ProgramExport(getActivity(), false); 35 | break; 36 | case 1: 37 | export = new ProgramExport(getActivity(), true); 38 | break; 39 | default: 40 | throw new IndexOutOfBoundsException(); 41 | } 42 | 43 | export.start(program, false); 44 | } 45 | }); 46 | 47 | 48 | return builder.create(); 49 | } 50 | 51 | public static ExportProgramDialogFragment create(Program program) { 52 | ExportProgramDialogFragment fragment = new ExportProgramDialogFragment(); 53 | 54 | fragment.setArguments(new Reference<>(program).to(new Bundle())); 55 | 56 | return fragment; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/GymLocator.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.content.Context; 4 | import android.content.res.AssetManager; 5 | import android.content.res.Resources; 6 | import android.database.sqlite.SQLiteDatabase; 7 | import android.os.Environment; 8 | import android.widget.Toast; 9 | 10 | import java.io.File; 11 | import java.io.IOException; 12 | 13 | import propoid.db.Locator; 14 | import propoid.db.locator.FileLocator; 15 | import propoid.util.content.Preference; 16 | 17 | /** 18 | */ 19 | class GymLocator implements Locator { 20 | 21 | private static final String NAME = "gym"; 22 | 23 | private final Context context; 24 | 25 | private SQLiteDatabase database; 26 | 27 | /** 28 | * Locate the database from the given context. 29 | * 30 | * @param context context 31 | */ 32 | public GymLocator(Context context) { 33 | this.context = context; 34 | } 35 | 36 | public SQLiteDatabase open() { 37 | if (database != null) { 38 | throw new IllegalStateException("already open"); 39 | } 40 | 41 | if (Preference.getBoolean(context, R.string.preference_data_external).get()) { 42 | try { 43 | database = open(external()); 44 | } catch (Exception ex) { 45 | Toast.makeText(context, R.string.gym_repository_extern_failed, Toast.LENGTH_LONG).show(); 46 | } 47 | } 48 | 49 | if (database == null) { 50 | database = open(internal()); 51 | } 52 | 53 | return database; 54 | } 55 | 56 | private File internal() { 57 | return context.getDatabasePath(NAME); 58 | } 59 | 60 | private File external() { 61 | return new File(Coxswain.getExternalFilesDir(context), NAME); 62 | } 63 | 64 | private SQLiteDatabase open(File file) { 65 | if (!file.exists()) { 66 | file.getParentFile().mkdirs(); 67 | } 68 | 69 | return SQLiteDatabase.openOrCreateDatabase(file, null); 70 | } 71 | 72 | @Override 73 | public void close() { 74 | database.close(); 75 | database = null; 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/util/PermissionActivity.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.util; 2 | 3 | import android.app.Activity; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.content.pm.PackageManager; 8 | import android.os.Bundle; 9 | 10 | import androidx.core.app.ActivityCompat; 11 | 12 | /** 13 | */ 14 | public class PermissionActivity extends Activity implements ActivityCompat.OnRequestPermissionsResultCallback { 15 | 16 | static final String ACTION = "svenmeier.coxswain.util.permission.GRANTED"; 17 | 18 | static final String PERMISSIONS = "permissions"; 19 | 20 | static final String GRANTED = "granted"; 21 | 22 | @Override 23 | protected void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | String[] permissions = getIntent().getStringArrayExtra(PERMISSIONS); 27 | 28 | ActivityCompat.requestPermissions(this, permissions, 1); 29 | } 30 | 31 | @Override 32 | public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 33 | boolean granted = true; 34 | for (int grantResult : grantResults) { 35 | granted &= (grantResult == PackageManager.PERMISSION_GRANTED); 36 | } 37 | 38 | Intent intent = new Intent(); 39 | intent.setAction(ACTION); 40 | intent.putExtra(PERMISSIONS, permissions); 41 | intent.putExtra(GRANTED, granted); 42 | sendBroadcast(intent); 43 | 44 | finish(); 45 | } 46 | 47 | public static IntentFilter start(Context context, String[] permissions) { 48 | Intent intent = new Intent(context, PermissionActivity.class); 49 | 50 | // required for activity started from non-activity 51 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 52 | 53 | intent.putExtra(PERMISSIONS, permissions); 54 | 55 | context.startActivity(intent); 56 | 57 | IntentFilter filter = new IntentFilter(); 58 | filter.addAction(PermissionActivity.ACTION); 59 | return filter; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/Protocol4Test.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired; 2 | 3 | import org.junit.Test; 4 | 5 | import svenmeier.coxswain.gym.Measurement; 6 | import svenmeier.coxswain.rower.wired.usb.ITransfer; 7 | import svenmeier.coxswain.rower.wired.usb.TestTransfer; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | */ 13 | public class Protocol4Test { 14 | 15 | Measurement measurement = new Measurement(); 16 | 17 | @Test 18 | public void test() throws Exception { 19 | TestTransfer transfer = new TestTransfer(); 20 | TestTrace trace = new TestTrace(); 21 | 22 | Protocol4 protocol = new Protocol4(transfer, trace); 23 | assertEquals(115200, transfer.baudrate); 24 | assertEquals(0, transfer.dataBits); 25 | assertEquals(TestTransfer.PARITY_NONE, transfer.parity); 26 | assertEquals(ITransfer.STOP_BIT_1_0, transfer.stopBits); 27 | assertEquals(false, transfer.tx); 28 | protocol.setThrottle(0); 29 | 30 | assertEquals(Protocol4.VERSION_UNKOWN, protocol.getVersion()); 31 | 32 | protocol.transfer(measurement); 33 | transfer.assertOutput("USB\r\n"); 34 | assertEquals(Protocol4.VERSION_UNKOWN, protocol.getVersion()); 35 | 36 | transfer.setupInput("_WR_\r\n"); 37 | protocol.transfer(measurement); 38 | transfer.assertOutput("IV?\r\n"); 39 | assertEquals(Protocol4.VERSION_UNKOWN, protocol.getVersion()); 40 | 41 | transfer.setupInput("IV42020\r\n"); 42 | protocol.transfer(measurement); 43 | assertEquals("42020", protocol.getVersion()); 44 | 45 | transfer.setupInput("IDT1E1151515\r\n"); 46 | protocol.transfer(measurement); 47 | assertEquals(((15 * 60) + 15)*60 +15, measurement.getDuration()); 48 | 49 | transfer.setupInput("IDT08A0003E8\r\n"); 50 | protocol.transfer(measurement); 51 | assertEquals(1, measurement.getEnergy()); 52 | 53 | // incomplete 54 | transfer.setupInput("IDT08A0003"); 55 | protocol.transfer(measurement); 56 | 57 | assertEquals("#protocol 4>USB<_WR_#handshake complete>IV?IRD140IRD057IRD14A>IRS1A9", trace.toString()); 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/RatioCalculator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower.wired; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | public class RatioCalculator { 21 | 22 | // actually would be 10 for one decimal place, but S4 23 | // reports pulls too late and recover too early - 24 | // thus we reduce the recovering phase 25 | public static final int MULTIPLIER = 8; 26 | 27 | public static final int MAX = 99; 28 | 29 | public boolean pulling = true; 30 | 31 | private long start = 0; 32 | 33 | private long pullDuration; 34 | 35 | private long recoverDuration; 36 | 37 | public void clear(long now) { 38 | pulling = true; 39 | start = now; 40 | 41 | pullDuration = 0; 42 | recoverDuration = 0; 43 | } 44 | 45 | public void strokeStart(Measurement measurement, long now) { 46 | if (pulling == false) { 47 | pulling = true; 48 | 49 | recoverDuration = (now - start); 50 | start = now; 51 | } 52 | } 53 | 54 | public void strokeEnd(Measurement measurement, long now) { 55 | if (pulling) { 56 | pulling = false; 57 | 58 | pullDuration = (now - start); 59 | start = now; 60 | 61 | int ratio = Math.min((int) (MULTIPLIER * recoverDuration / pullDuration), MAX); 62 | measurement.setStrokeRatio(ratio); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/SpeedAdjuster.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | /** 21 | * Ignore speed and calculate from power instead. 22 | */ 23 | public class SpeedAdjuster extends Adjuster { 24 | 25 | private int cms; 26 | 27 | public SpeedAdjuster(Measurement measurement) { 28 | super(measurement); 29 | } 30 | 31 | @Override 32 | public void reset() { 33 | super.reset(); 34 | 35 | cms = 0; 36 | } 37 | 38 | @Override 39 | public void setSpeed(int untrustedSpeed) { 40 | // magic formula see: 41 | // http://www.concept2.com/indoor-rowers/training/calculators/watts-calculator 42 | float mps = 0.709492f * (float) Math.pow(getPower(), 1d / 3d); 43 | 44 | super.setSpeed(Math.round(mps * 100)); 45 | } 46 | 47 | @Override 48 | public void setDistance(int untrustedDistance) { 49 | if (untrustedDistance == 0) { 50 | super.setDistance(0); 51 | } 52 | } 53 | 54 | @Override 55 | public void setDuration(int newDuration) { 56 | int oldDuration = super.getDuration(); 57 | 58 | super.setDuration(newDuration); 59 | 60 | if (newDuration > oldDuration) { 61 | int delta = newDuration - oldDuration; 62 | 63 | cms += delta * getSpeed(); 64 | 65 | super.setDistance(cms / 100); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/usb/UsbConnector.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired.usb; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.hardware.usb.UsbDevice; 9 | import android.hardware.usb.UsbManager; 10 | 11 | import java.util.Collection; 12 | 13 | /** 14 | * Created by sven on 24.10.15. 15 | */ 16 | public class UsbConnector extends BroadcastReceiver { 17 | 18 | private static final String DEVICE_CONNECT = "svenmeier.coxswain.usb.lister"; 19 | 20 | private final Context context; 21 | 22 | private final UsbManager manager; 23 | 24 | public UsbConnector(Context context) { 25 | this.context = context; 26 | 27 | manager = (UsbManager) context.getSystemService(Context.USB_SERVICE); 28 | 29 | context.registerReceiver(this, new IntentFilter(DEVICE_CONNECT)); 30 | } 31 | 32 | public void destroy() { 33 | context.unregisterReceiver(this); 34 | } 35 | 36 | public Collection list() { 37 | return manager.getDeviceList().values(); 38 | } 39 | 40 | /** 41 | * Connect to the given device. 42 | * 43 | * @see #onConnected(UsbDevice) 44 | */ 45 | public void connect(UsbDevice device) { 46 | 47 | PendingIntent intent = PendingIntent.getBroadcast(context, 0, new Intent(DEVICE_CONNECT), 0); 48 | 49 | manager.requestPermission(device, intent); 50 | 51 | } 52 | 53 | @Override 54 | public void onReceive(Context context, Intent intent) { 55 | String action = intent.getAction(); 56 | 57 | if (DEVICE_CONNECT.equals(action)) { 58 | synchronized (this) { 59 | UsbDevice device = (UsbDevice)intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); 60 | 61 | if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { 62 | onConnected(device); 63 | } 64 | } 65 | } 66 | 67 | } 68 | 69 | /** 70 | * Callback when device was connected. 71 | * 72 | * @see #connect(UsbDevice) 73 | */ 74 | protected void onConnected(UsbDevice device) { 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/DeleteDialogFragment.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.view; 17 | 18 | import android.app.AlertDialog; 19 | import android.app.Dialog; 20 | import android.content.DialogInterface; 21 | import android.os.Bundle; 22 | 23 | import androidx.fragment.app.DialogFragment; 24 | 25 | import propoid.core.Propoid; 26 | import propoid.db.Reference; 27 | import svenmeier.coxswain.Gym; 28 | import svenmeier.coxswain.R; 29 | 30 | /** 31 | */ 32 | public class DeleteDialogFragment extends DialogFragment implements DialogInterface.OnClickListener { 33 | 34 | @Override 35 | public Dialog onCreateDialog(Bundle savedInstanceState) { 36 | return new AlertDialog.Builder(getActivity()) 37 | .setTitle(R.string.action_delete_confirm_title) 38 | .setMessage(R.string.action_delete_confirm_message).setPositiveButton(R.string.action_delete_confirm, this) 39 | .create(); 40 | } 41 | 42 | @Override 43 | public void onClick(DialogInterface dialogInterface, int i) { 44 | Propoid propoid = Gym.instance(getActivity()).get(Reference.from(getArguments())); 45 | Gym.instance(getActivity()).delete(propoid); 46 | 47 | dismiss(); 48 | } 49 | 50 | public static DeleteDialogFragment create(Propoid propoid) { 51 | DeleteDialogFragment fragment = new DeleteDialogFragment(); 52 | 53 | fragment.setArguments(new Reference<>(propoid).to(new Bundle())); 54 | 55 | return fragment; 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/Energy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import android.content.Context; 19 | 20 | import propoid.util.content.Preference; 21 | import svenmeier.coxswain.R; 22 | 23 | /** 24 | */ 25 | public class Energy { 26 | 27 | private static final float KCAL_TO_WH = 1.163f; 28 | 29 | private static final float KCAL_TO_KJ = 4.185f; 30 | 31 | private Context context; 32 | 33 | private int kcal; 34 | 35 | public String formatted() { 36 | Preference unit = Preference.getString(context, R.string.preference_energy_unit); 37 | 38 | switch (unit.get()) { 39 | case "kJ": 40 | return String.format(context.getString(R.string.energy_kilojoules), kj()); 41 | case "Wh": 42 | return String.format(context.getString(R.string.energy_watthours), wh()); 43 | default: 44 | return String.format(context.getString(R.string.energy_kilocalories), kcal); 45 | } 46 | } 47 | 48 | public int wh() { 49 | return Math.round(kcal * KCAL_TO_WH); 50 | } 51 | 52 | public int kj() { 53 | return Math.round(KCAL_TO_KJ * kcal); 54 | } 55 | 56 | public int kcal() { 57 | return kcal; 58 | } 59 | 60 | public static Energy kcal(Context context, int kcal) { 61 | Energy energy = new Energy(); 62 | 63 | energy.context = context; 64 | energy.kcal = kcal; 65 | 66 | return energy; 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/preference/EditTextPreference.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view.preference; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | 6 | import androidx.preference.Preference; 7 | 8 | /** 9 | * An specialization that substitutes the current text into the summary (as ListPreference does it 10 | * too). 11 | */ 12 | public class EditTextPreference extends androidx.preference.EditTextPreference { 13 | 14 | public EditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { 15 | super(context, attrs, defStyleAttr); 16 | 17 | init(context, attrs); 18 | } 19 | 20 | public EditTextPreference(Context context, AttributeSet attrs) { 21 | super(context, attrs); 22 | 23 | init(context, attrs); 24 | } 25 | 26 | public EditTextPreference(Context context) { 27 | super(context); 28 | } 29 | 30 | private void init(Context context, AttributeSet attrs) { 31 | try { 32 | for (int i=0;i 10 | 11 | 16 | 17 | 24 | 25 | 30 | 31 | 42 | 49 | 50 | 51 | 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/util/PermissionBlock.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.util; 2 | 3 | import android.content.BroadcastReceiver; 4 | import android.content.Context; 5 | import android.content.Intent; 6 | import android.content.IntentFilter; 7 | import android.content.pm.PackageManager; 8 | 9 | import androidx.core.content.ContextCompat; 10 | 11 | import java.util.Arrays; 12 | 13 | public class PermissionBlock { 14 | 15 | private final Context context; 16 | 17 | private String[] permissions; 18 | 19 | private BroadcastReceiverImpl receiver; 20 | 21 | public PermissionBlock(Context context) { 22 | this.context = context; 23 | } 24 | 25 | public void acquirePermissions(String... permissions) { 26 | unregister(); 27 | 28 | this.permissions = permissions; 29 | 30 | for (String permission : permissions) { 31 | if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { 32 | requestPermissions(); 33 | return; 34 | } 35 | } 36 | 37 | onPermissionsApproved(); 38 | } 39 | 40 | protected final void abortPermissions() { 41 | unregister(); 42 | } 43 | 44 | protected void onPermissionsApproved() { 45 | } 46 | 47 | protected void onRejected() { 48 | } 49 | 50 | private void requestPermissions() { 51 | 52 | IntentFilter filter = PermissionActivity.start(context, permissions); 53 | 54 | receiver = new BroadcastReceiverImpl(); 55 | context.registerReceiver(receiver, filter); 56 | } 57 | 58 | private void unregister() { 59 | if (receiver != null) { 60 | context.unregisterReceiver(receiver); 61 | receiver = null; 62 | } 63 | } 64 | 65 | private class BroadcastReceiverImpl extends BroadcastReceiver { 66 | @Override 67 | public final void onReceive(Context context, Intent intent) { 68 | 69 | String[] permissions = intent.getStringArrayExtra(PermissionActivity.PERMISSIONS); 70 | if (Arrays.equals(PermissionBlock.this.permissions, permissions) == false) { 71 | return; 72 | } 73 | 74 | unregister(); 75 | 76 | boolean granted = intent.getBooleanExtra(PermissionActivity.GRANTED, false); 77 | if (granted) { 78 | onPermissionsApproved(); 79 | } else { 80 | onRejected(); 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/Program2Json.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.location.Location; 4 | import android.util.JsonWriter; 5 | 6 | import java.io.IOException; 7 | import java.io.Writer; 8 | import java.util.List; 9 | 10 | import propoid.core.Property; 11 | import svenmeier.coxswain.gym.Difficulty; 12 | import svenmeier.coxswain.gym.Program; 13 | import svenmeier.coxswain.gym.Segment; 14 | import svenmeier.coxswain.gym.Snapshot; 15 | import svenmeier.coxswain.gym.Workout; 16 | 17 | /** 18 | * Converter for {@link svenmeier.coxswain.gym.Program}s. 19 | */ 20 | public class Program2Json { 21 | 22 | private JsonWriter writer; 23 | 24 | public Program2Json(Writer writer) throws IOException { 25 | this.writer = new JsonWriter(writer); 26 | this.writer.setIndent(" "); 27 | } 28 | 29 | public void document(Program program) throws IOException { 30 | program(program); 31 | 32 | writer.flush(); 33 | } 34 | 35 | private void program(Program program) throws IOException { 36 | writer.beginObject(); 37 | 38 | writer.name("name").value(program.name.get()); 39 | 40 | writer.name("segments"); 41 | writer.beginArray(); 42 | for (Segment segment : program.getSegments()) { 43 | segment(segment); 44 | } 45 | writer.endArray(); 46 | 47 | writer.endObject(); 48 | } 49 | 50 | private void segment(Segment segment) throws IOException { 51 | writer.beginObject(); 52 | 53 | writer.name("difficulty").value(segment.difficulty.get().name()); 54 | 55 | target("distance", segment.distance); 56 | target("duration", segment.duration); 57 | target("strokes", segment.strokes); 58 | target("energy", segment.energy); 59 | 60 | limit("speed", segment.speed); 61 | limit("strokeRate", segment.strokeRate); 62 | limit("pulse", segment.pulse); 63 | limit("power", segment.power); 64 | 65 | writer.endObject(); 66 | } 67 | 68 | private void target(String name, Property property) throws IOException { 69 | if (property.get() > 0) { 70 | writer.name(name).value(property.get()); 71 | } 72 | } 73 | 74 | private void limit(String name, Property property) throws IOException { 75 | if (property.get() > 0) { 76 | writer.name(name).value(property.get()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/ImportIntention.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.database.Cursor; 6 | import android.net.Uri; 7 | import android.provider.OpenableColumns; 8 | import android.util.Log; 9 | import android.widget.Toast; 10 | 11 | import svenmeier.coxswain.Coxswain; 12 | import svenmeier.coxswain.R; 13 | import svenmeier.coxswain.garmin.TcxImport; 14 | 15 | /** 16 | */ 17 | public class ImportIntention { 18 | 19 | private final Activity activity; 20 | 21 | public ImportIntention(Activity activity) { 22 | this.activity = activity; 23 | } 24 | 25 | 26 | public boolean onIntent(Intent intent) { 27 | Uri uri; 28 | if (Intent.ACTION_VIEW.equals(intent.getAction())) { 29 | uri = intent.getData(); 30 | } else if (Intent.ACTION_SEND.equals(intent.getAction())) { 31 | uri = intent.getParcelableExtra(Intent.EXTRA_STREAM); 32 | } else { 33 | return false; 34 | } 35 | 36 | return importFrom(uri); 37 | } 38 | 39 | public boolean importFrom(Uri uri) { 40 | Import importer = null; 41 | 42 | try { 43 | String name = getFileName(uri); 44 | int dot = name.lastIndexOf('.'); 45 | String extension = name.substring(dot + 1); 46 | 47 | if ("tcx".equalsIgnoreCase(extension)) { 48 | importer = new TcxImport(activity); 49 | } else if ("coxswain".equalsIgnoreCase(extension)) { 50 | importer = new ProgramImport(activity); 51 | } 52 | } catch (Exception ex) { 53 | Log.e(Coxswain.TAG, ex.getMessage()); 54 | } 55 | 56 | if (importer == null) { 57 | Toast.makeText(activity, R.string.import_unknown, Toast.LENGTH_LONG).show(); 58 | return false; 59 | } 60 | 61 | importer.start(uri); 62 | return true; 63 | } 64 | 65 | private String getFileName(Uri uri) { 66 | Cursor cursor = activity.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null); 67 | 68 | if (cursor == null) { 69 | return uri.getLastPathSegment(); 70 | } else { 71 | try { 72 | int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); 73 | cursor.moveToFirst(); 74 | return cursor.getString(nameIndex); 75 | } finally { 76 | cursor.close(); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/TCX2Course.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.location.Location; 4 | 5 | import java.io.IOException; 6 | import java.io.Reader; 7 | import java.text.ParseException; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | import propoid.util.io.XmlNavigator; 12 | 13 | /** 14 | * Converter for {@code TCX} (Training Center XML). 15 | */ 16 | public class TCX2Course { 17 | 18 | private final XmlNavigator navigator; 19 | 20 | private Course course; 21 | 22 | public TCX2Course(Reader reader) throws IOException { 23 | navigator = new XmlNavigator(reader); 24 | } 25 | 26 | public Course getCourse() { 27 | return course; 28 | } 29 | 30 | public TCX2Course course() throws IOException, ParseException { 31 | if (navigator.descent("Course") == false) { 32 | throw new ParseException(" missing", navigator.offset()); 33 | } 34 | 35 | if (navigator.descent("Track") == false) { 36 | throw new ParseException(" missing", navigator.offset()); 37 | } 38 | 39 | this.course = new Course(trackpoints()); 40 | 41 | navigator.ascent(); 42 | 43 | navigator.ascent(); 44 | 45 | return this; 46 | } 47 | 48 | private List trackpoints() throws IOException { 49 | List trackpoints = new ArrayList<>(); 50 | 51 | while (navigator.descent("Trackpoint")) { 52 | Course.Trackpoint trackpoint = new Course.Trackpoint(location(), distanceMeters()); 53 | 54 | trackpoints.add(trackpoint); 55 | 56 | navigator.ascent(); 57 | } 58 | 59 | return trackpoints; 60 | } 61 | 62 | private double distanceMeters() throws IOException { 63 | navigator.descentRequired("DistanceMeters"); 64 | 65 | double distanceMeters = Double.valueOf(navigator.getText()); 66 | 67 | navigator.ascent(); 68 | 69 | return distanceMeters; 70 | } 71 | 72 | private Location location() throws IOException { 73 | Location location = new Location(""); 74 | 75 | if (navigator.descent("Position")) { 76 | location.setLatitude(Double.parseDouble(navigator.getText("LatitudeDegrees"))); 77 | location.setLongitude(Double.parseDouble(navigator.getText("LongitudeDegrees"))); 78 | 79 | navigator.ascent(); 80 | } 81 | 82 | return location; 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_workouts_item.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 24 | 25 | 30 | 31 | 38 | 39 | 40 | 52 | 53 | 54 | 62 | 63 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/EnergyAdjuster.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | /** 21 | * Adjust energy relative to weight. 22 | */ 23 | public class EnergyAdjuster extends Adjuster { 24 | 25 | private static final int WEIGHT_MIN = 40; 26 | 27 | private static final int WEIGHT_MAX = 160; 28 | 29 | private static final int DEFAULT_WEIGHT = 68; 30 | 31 | private static final double S4_CALORIES_FOR_WEIGHT = 257.1; 32 | 33 | private static final double CALORIES_FACTOR = 1.714; 34 | 35 | private static final double KG_TO_POUNDS = 2.20462; 36 | 37 | private int weight; 38 | 39 | /** 40 | * Calculate with {@link #DEFAULT_WEIGHT}, i.e. don't adjust. 41 | */ 42 | public EnergyAdjuster(Measurement measurement) { 43 | this(measurement, DEFAULT_WEIGHT); 44 | } 45 | 46 | public EnergyAdjuster(Measurement measurement, int weight) { 47 | super(measurement); 48 | 49 | if (weight < WEIGHT_MIN) { 50 | weight = WEIGHT_MIN; 51 | } 52 | 53 | if (weight > WEIGHT_MAX) { 54 | weight = WEIGHT_MAX; 55 | } 56 | 57 | this.weight = weight; 58 | } 59 | 60 | @Override 61 | public void setEnergy(int energy) { 62 | if (getDistance() == 0) { 63 | // leave unadjusted 64 | super.setEnergy(energy); 65 | } else { 66 | double adjusted = ((double)energy) - S4_CALORIES_FOR_WEIGHT + (CALORIES_FACTOR * (weight * KG_TO_POUNDS)); 67 | 68 | super.setEnergy(Math.max(0, (int)adjusted)); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/charts/XAxisRenderer2.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view.charts; 2 | 3 | import android.graphics.Canvas; 4 | import android.graphics.Paint; 5 | import android.graphics.Path; 6 | 7 | import com.github.mikephil.charting.charts.LineChart; 8 | import com.github.mikephil.charting.components.LimitLine; 9 | import com.github.mikephil.charting.components.XAxis; 10 | import com.github.mikephil.charting.components.YAxis; 11 | import com.github.mikephil.charting.renderer.XAxisRenderer; 12 | import com.github.mikephil.charting.utils.Transformer; 13 | import com.github.mikephil.charting.utils.ViewPortHandler; 14 | 15 | /** 16 | * Support for rendering {@link LimitArea}. 17 | */ 18 | public class XAxisRenderer2 extends XAxisRenderer { 19 | 20 | private Paint mLimitAreaPaint; 21 | 22 | public XAxisRenderer2(LineChart chartView) { 23 | super(chartView.getViewPortHandler(), chartView.getXAxis(), chartView.getTransformer(YAxis.AxisDependency.LEFT)); 24 | 25 | mLimitAreaPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 26 | mLimitAreaPaint.setStyle(Paint.Style.FILL); 27 | } 28 | 29 | protected float[] mRenderLimitAreaBuffer = new float[2]; 30 | private Path mLimitAreaPath = new Path(); 31 | 32 | public void renderLimitLineLine(Canvas c, LimitLine limitLine, float[] position1) { 33 | 34 | if (limitLine instanceof LimitArea) { 35 | float[] position2 = mRenderLimitAreaBuffer; 36 | position2[0] = ((LimitArea) limitLine).getMinimum(); 37 | position2[1] = 0.f; 38 | 39 | mTrans.pointValuesToPixel(position2); 40 | 41 | mLimitAreaPath.reset(); 42 | mLimitAreaPath.moveTo(position2[0], mViewPortHandler.contentTop()); 43 | mLimitAreaPath.lineTo(position1[0], mViewPortHandler.contentTop()); 44 | mLimitAreaPath.lineTo(position1[0], mViewPortHandler.contentBottom()); 45 | mLimitAreaPath.lineTo(position2[0], mViewPortHandler.contentBottom()); 46 | mLimitAreaPath.close(); 47 | 48 | mLimitAreaPaint.setColor(limitLine.getLineColor()); 49 | 50 | c.drawPath(mLimitAreaPath, mLimitAreaPaint); 51 | } else { 52 | super.renderLimitLineLine(c, limitLine, position1); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/Coxswain.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain; 2 | 3 | import android.app.Application; 4 | import android.app.Notification; 5 | import android.app.NotificationChannel; 6 | import android.app.NotificationManager; 7 | import android.content.Context; 8 | import android.os.Build; 9 | import android.os.Environment; 10 | 11 | import androidx.annotation.RequiresApi; 12 | import androidx.preference.PreferenceManager; 13 | 14 | import java.io.File; 15 | 16 | /** 17 | */ 18 | public class Coxswain extends Application { 19 | 20 | public static String TAG = "coxswain"; 21 | 22 | private Gym gym; 23 | 24 | public static void initNotification(Context context, Notification.Builder builder, String name) { 25 | NotificationManager notificationManager = (NotificationManager)context.getSystemService(NOTIFICATION_SERVICE); 26 | 27 | builder.setSmallIcon(R.drawable.notification); 28 | builder.setContentTitle(context.getString(R.string.app_name)); 29 | builder.setDefaults(Notification.DEFAULT_VIBRATE); 30 | builder.setPriority(Notification.PRIORITY_DEFAULT); 31 | 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 33 | NotificationChannel channel = new NotificationChannel(name.toLowerCase(), 34 | name, NotificationManager.IMPORTANCE_DEFAULT); 35 | channel.enableVibration(true); 36 | channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC); 37 | 38 | notificationManager.createNotificationChannel(channel); 39 | builder.setChannelId(channel.getId()); 40 | } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 41 | builder.setVisibility(Notification.VISIBILITY_PUBLIC); 42 | } 43 | } 44 | 45 | public static File getExternalFilesDir(Context context) { 46 | // for API 29 only possible with android:requestLegacyExternalStorage="true" 47 | // 48 | // in future we have to use context.getExternalFilesDir(null); 49 | return Environment.getExternalStoragePublicDirectory(Coxswain.TAG); 50 | } 51 | 52 | @Override 53 | public void onCreate() { 54 | super.onCreate(); 55 | 56 | PreferenceManager.setDefaultValues(this, R.xml.preferences, true); 57 | 58 | gym = Gym.instance(this); 59 | 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 61 | CompactService.setup(this.getApplicationContext()); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/ExportWorkoutDialogFragment.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view; 2 | 3 | import android.app.AlertDialog; 4 | import android.app.Dialog; 5 | import android.content.DialogInterface; 6 | import android.os.Bundle; 7 | 8 | import androidx.fragment.app.DialogFragment; 9 | 10 | import propoid.db.Reference; 11 | import svenmeier.coxswain.garmin.TcxShareExport; 12 | import svenmeier.coxswain.io.Export; 13 | import svenmeier.coxswain.Gym; 14 | import svenmeier.coxswain.R; 15 | import svenmeier.coxswain.io.CalendarExport; 16 | import svenmeier.coxswain.garmin.TcxExport; 17 | import svenmeier.coxswain.google.FitExport; 18 | import svenmeier.coxswain.gym.Workout; 19 | 20 | public class ExportWorkoutDialogFragment extends DialogFragment { 21 | 22 | private Export export; 23 | 24 | @Override 25 | public Dialog onCreateDialog(Bundle savedInstanceState) { 26 | final Workout workout = Gym.instance(getActivity()).get(Reference.from(getArguments())); 27 | 28 | AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); 29 | 30 | String[] exports = new String[]{getString(R.string.calendar_export), getString(R.string.garmin_export), getString(R.string.garmin_export_share), getString(R.string.googlefit_export)}; 31 | 32 | builder.setTitle(R.string.action_export) 33 | .setItems(exports, new DialogInterface.OnClickListener() { 34 | public void onClick(DialogInterface dialog, int which) { 35 | switch (which) { 36 | case 0: 37 | export = new CalendarExport(getActivity()); 38 | break; 39 | case 1: 40 | export = new TcxExport(getActivity()); 41 | break; 42 | case 2: 43 | export = new TcxShareExport(getActivity()); 44 | break; 45 | case 3: 46 | export = new FitExport(getActivity()); 47 | break; 48 | default: 49 | throw new IndexOutOfBoundsException(); 50 | } 51 | 52 | Export.start(getActivity(), export, workout); 53 | } 54 | }); 55 | 56 | 57 | return builder.create(); 58 | } 59 | 60 | public static ExportWorkoutDialogFragment create(Workout workout) { 61 | ExportWorkoutDialogFragment fragment = new ExportWorkoutDialogFragment(); 62 | 63 | fragment.setArguments(new Reference<>(workout).to(new Bundle())); 64 | 65 | return fragment; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/Utils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.view; 17 | 18 | import android.app.Activity; 19 | import android.app.Application; 20 | import android.content.Context; 21 | import android.util.TypedValue; 22 | 23 | import androidx.fragment.app.Fragment; 24 | 25 | /** 26 | * Created by sven on 19.08.15. 27 | */ 28 | public class Utils { 29 | 30 | public static float dpToPx(Context context, int dp) { 31 | 32 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); 33 | } 34 | 35 | @SuppressWarnings("unchecked") 36 | public static T getCallback(Fragment fragment, Class callback) { 37 | while (true) { 38 | if (callback.isInstance(fragment)) { 39 | return (T) fragment; 40 | } 41 | 42 | Fragment parentFragment = fragment.getParentFragment(); 43 | if (parentFragment == null) { 44 | break; 45 | } 46 | fragment = parentFragment; 47 | } 48 | 49 | return getCallback(fragment.getActivity(), callback); 50 | } 51 | 52 | @SuppressWarnings("unchecked") 53 | public static T getCallback(Activity activity, Class callback) { 54 | if (activity != null && callback.isInstance(activity)) { 55 | return (T) activity; 56 | } 57 | 58 | Application application = activity.getApplication(); 59 | if (application != null && callback.isInstance(application)) { 60 | return (T) application; 61 | } 62 | 63 | throw new IllegalStateException("no requested parental callback " + callback.getSimpleName()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/Export.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.net.Uri; 6 | import android.widget.Toast; 7 | 8 | import androidx.core.content.FileProvider; 9 | 10 | import java.io.File; 11 | 12 | import propoid.util.content.Preference; 13 | import svenmeier.coxswain.R; 14 | import svenmeier.coxswain.gym.Workout; 15 | 16 | /** 17 | */ 18 | public abstract class Export { 19 | 20 | protected final Context context; 21 | 22 | protected Export(Context context) { 23 | this.context = context; 24 | } 25 | 26 | public abstract void start(T t, boolean automatic); 27 | 28 | /** 29 | * Start an automatic export for the given {@link Workout}. 30 | * 31 | * @param context context 32 | * @param workout workout 33 | */ 34 | public static void start(Context context, Workout workout) { 35 | Preference auto = Preference.getBoolean(context, R.string.preference_export_auto); 36 | if (auto.get()) { 37 | Preference last = Preference.getString(context, R.string.preference_export_last); 38 | 39 | Export export; 40 | 41 | String name = last.get(); 42 | try { 43 | export = (Export) Class.forName(name).getConstructor(Context.class).newInstance(context); 44 | } catch (Exception ex) { 45 | Toast.makeText(context, context.getString(R.string.preference_export_auto_reminder), Toast.LENGTH_LONG).show(); 46 | return; 47 | } 48 | 49 | export.start(workout, true); 50 | } 51 | } 52 | 53 | /** 54 | * Start a specific export for the given {@link Workout}, enabling it for any successive automatic 55 | * export. 56 | * 57 | * @param context context 58 | * @param workout workout 59 | */ 60 | public static void start(Context context, Export export, Workout workout) { 61 | Preference last = Preference.getString(context, R.string.preference_export_last); 62 | 63 | last.set(export.getClass().getName()); 64 | 65 | export.start(workout, false); 66 | } 67 | 68 | /** 69 | * Set a file on the given intent. 70 | */ 71 | public static void setFile(Context context, File file, Intent intent) { 72 | Uri uri = FileProvider.getUriForFile(context, context.getPackageName() + ".files", file); 73 | intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); 74 | intent.putExtra(Intent.EXTRA_STREAM, uri); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/Distance.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import android.content.Context; 19 | 20 | import propoid.util.content.Preference; 21 | import svenmeier.coxswain.R; 22 | 23 | /** 24 | */ 25 | public class Distance { 26 | 27 | private static final float M_TO_MI = 0.000621371f; 28 | 29 | private static final float M_TO_YD = 1.09361f; 30 | 31 | private static final float M_TO_FT = 3.28084f; 32 | 33 | private Context context; 34 | 35 | private int m; 36 | 37 | public String formatted() { 38 | Preference unit = Preference.getString(context, R.string.preference_distance_unit); 39 | 40 | switch (unit.get()) { 41 | case "mi": 42 | return String.format(context.getString(R.string.distance_miles), mi()); 43 | case "yd": 44 | return String.format(context.getString(R.string.distance_yards), yd()); 45 | case "ft": 46 | return String.format(context.getString(R.string.distance_feets), ft()); 47 | default: 48 | return String.format(context.getString(R.string.distance_meters), m); 49 | } 50 | } 51 | 52 | public int mi() { 53 | return Math.round(m * M_TO_MI); 54 | } 55 | 56 | public int yd() { 57 | return Math.round(m * M_TO_YD); 58 | } 59 | 60 | public int ft() { 61 | return Math.round(m * M_TO_FT); 62 | } 63 | 64 | public int m() { 65 | return m; 66 | } 67 | 68 | public static Distance m(Context context, int m) { 69 | Distance distance = new Distance(); 70 | 71 | distance.context = context; 72 | distance.m = m; 73 | 74 | return distance; 75 | } 76 | } -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/rower/wired/usb/TestTransfer.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.rower.wired.usb; 2 | 3 | import svenmeier.coxswain.rower.wired.usb.ITransfer; 4 | 5 | import static org.junit.Assert.assertEquals; 6 | 7 | /** 8 | */ 9 | public class TestTransfer implements ITransfer { 10 | 11 | public int baudrate; 12 | public int bufferLength = 0; 13 | public int dataBits; 14 | public int parity; 15 | public int stopBits; 16 | public boolean tx; 17 | 18 | public byte[] buffer = new byte[256]; 19 | 20 | @Override 21 | public void setBaudrate(int baudrate) { 22 | this.baudrate = baudrate; 23 | } 24 | 25 | @Override 26 | public void setData(int dataBits, int parity, int stopBits, boolean tx) { 27 | this.dataBits = dataBits; 28 | this.parity = parity; 29 | this.stopBits = stopBits; 30 | this.tx = tx; 31 | } 32 | 33 | @Override 34 | public void setTimeout(int timeout) { 35 | } 36 | 37 | public void setupInput(String buffer) { 38 | for (int b = 0; b < buffer.length(); b++) { 39 | this.buffer[b] = (byte) buffer.charAt(b); 40 | } 41 | 42 | this.bufferLength = buffer.length(); 43 | } 44 | 45 | public void setupInput(byte[] buffer) { 46 | System.arraycopy(buffer, 0, this.buffer, 0, buffer.length); 47 | 48 | this.bufferLength = buffer.length; 49 | } 50 | 51 | public void assertOutput(String string) { 52 | assertEquals(string.length(), this.bufferLength); 53 | 54 | for (int b = 0; b < string.length(); b++) { 55 | assertEquals((byte) string.charAt(b), this.buffer[b]); 56 | } 57 | 58 | this.bufferLength = 0; 59 | } 60 | 61 | public void assertOutput(byte[] buffer) { 62 | assertEquals(buffer.length, this.bufferLength); 63 | 64 | for (int b = 0; b < buffer.length; b++) { 65 | assertEquals(buffer[b], this.buffer[b]); 66 | } 67 | 68 | this.bufferLength = 0; 69 | } 70 | 71 | @Override 72 | public void produce(byte[] b) { 73 | System.arraycopy(b, 0, buffer, 0, b.length); 74 | bufferLength = b.length; 75 | } 76 | 77 | @Override 78 | public Consumer consumer() { 79 | return new Consumer() { 80 | @Override 81 | protected byte[] getBuffer() { 82 | return buffer; 83 | } 84 | 85 | @Override 86 | protected int getBufferLength() { 87 | return bufferLength; 88 | } 89 | 90 | @Override 91 | protected void setBufferLength(int bufferLength) { 92 | TestTransfer.this.bufferLength = bufferLength; 93 | } 94 | }; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/Json2Program.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.util.JsonReader; 4 | 5 | import java.io.IOException; 6 | import java.io.Reader; 7 | import java.util.ArrayList; 8 | 9 | import propoid.core.Property; 10 | import svenmeier.coxswain.gym.Difficulty; 11 | import svenmeier.coxswain.gym.Program; 12 | import svenmeier.coxswain.gym.Segment; 13 | 14 | /** 15 | * Converter for {@link Program}s. 16 | */ 17 | public class Json2Program { 18 | 19 | private JsonReader reader; 20 | 21 | public Json2Program(Reader reader) throws IOException { 22 | this.reader = new JsonReader(reader); 23 | } 24 | 25 | public Program program() throws IOException { 26 | Program program = new Program(); 27 | 28 | reader.beginObject(); 29 | 30 | require("name"); 31 | program.name.set(reader.nextString()); 32 | 33 | program.segments.set(new ArrayList()); 34 | require("segments"); 35 | reader.beginArray(); 36 | while (reader.hasNext()) { 37 | program.segments.get().add(segment()); 38 | } 39 | 40 | reader.endArray(); 41 | reader.endObject(); 42 | 43 | return program; 44 | } 45 | 46 | private void require(String name) throws IOException { 47 | if (name.equals(reader.nextName()) == false) { 48 | throw new IOException("'" + name + "' expected"); 49 | } 50 | } 51 | 52 | private Segment segment() throws IOException { 53 | Segment segment = new Segment(); 54 | 55 | reader.beginObject(); 56 | 57 | require("difficulty"); 58 | 59 | segment.difficulty.set(Difficulty.valueOf(reader.nextString())); 60 | 61 | while (reader.hasNext()) { 62 | switch (reader.nextName()) { 63 | case "distance": 64 | segment.setDistance(reader.nextInt()); 65 | break; 66 | case "duration": 67 | segment.setDuration(reader.nextInt()); 68 | break; 69 | case "strokes": 70 | segment.setStrokes(reader.nextInt()); 71 | break; 72 | case "energy": 73 | segment.setEnergy(reader.nextInt()); 74 | break; 75 | case "speed": 76 | segment.setSpeed(reader.nextInt()); 77 | break; 78 | case "strokeRate": 79 | segment.setStrokeRate(reader.nextInt()); 80 | break; 81 | case "pulse": 82 | segment.setPulse(reader.nextInt()); 83 | break; 84 | case "power": 85 | segment.setPower(reader.nextInt()); 86 | break; 87 | } 88 | } 89 | 90 | reader.endObject(); 91 | 92 | return segment; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/google/Workout2FitTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.google; 2 | 3 | import com.google.android.gms.fitness.data.DataSet; 4 | import com.google.android.gms.fitness.data.DataType; 5 | import com.google.android.gms.fitness.data.Session; 6 | import com.google.android.gms.fitness.request.SessionInsertRequest; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.robolectric.RobolectricTestRunner; 11 | import org.robolectric.annotation.Config; 12 | 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | import java.util.Collection; 16 | import java.util.Iterator; 17 | import java.util.List; 18 | 19 | import svenmeier.coxswain.gym.Snapshot; 20 | import svenmeier.coxswain.gym.Workout; 21 | 22 | import static org.junit.Assert.assertEquals; 23 | import static org.junit.Assert.assertFalse; 24 | 25 | /** 26 | * Test for {@link Workout2Fit}. 27 | */ 28 | @RunWith(RobolectricTestRunner.class) 29 | @Config(constants = svenmeier.coxswain.BuildConfig.class) 30 | public class Workout2FitTest { 31 | 32 | private static final long Mon_Jun_15_2015 = 1434326400000l; 33 | 34 | @Test 35 | public void snapshots() throws IOException { 36 | Workout workout = new Workout(); 37 | workout.start.set(Mon_Jun_15_2015 + (60 * 1000)); 38 | workout.duration.set(2); 39 | workout.distance.set(6); 40 | workout.strokes.set(2); 41 | workout.energy.set(3); 42 | 43 | List snapshots = new ArrayList<>(); 44 | 45 | for (int i = 0; i < Workout2Fit.MAX_DATAPOINTS + 1; i++) { 46 | Snapshot snapshot = new Snapshot(); 47 | snapshot.speed.set(4_50); 48 | snapshot.pulse.set(80); 49 | snapshot.strokeRate.set(25); 50 | snapshot.distance.set(2); 51 | snapshot.strokes.set(0); 52 | snapshots.add(snapshot); 53 | } 54 | Workout2Fit workout2Fit = new Workout2Fit(); 55 | Session session = workout2Fit.session(workout); 56 | 57 | Collection mappers = workout2Fit.mappers(); 58 | 59 | Iterator mapper = mappers.iterator(); 60 | assertEquals(DataType.AGGREGATE_CALORIES_EXPENDED, mapper.next().type()); 61 | assertEquals(DataType.AGGREGATE_DISTANCE_DELTA, mapper.next().type()); 62 | assertEquals(DataType.TYPE_SPEED, mapper.next().type()); 63 | assertEquals(DataType.TYPE_HEART_RATE_BPM, mapper.next().type()); 64 | assertEquals(DataType.TYPE_POWER_SAMPLE, mapper.next().type()); 65 | assertFalse(mapper.hasNext()); 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/gym/WorkoutTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.gym; 2 | 3 | import org.junit.Test; 4 | 5 | import static junit.framework.Assert.assertEquals; 6 | 7 | /** 8 | * Test for {@link Workout}. 9 | */ 10 | public class WorkoutTest { 11 | 12 | @Test 13 | public void measurement() { 14 | Measurement measurement = new Measurement(); 15 | 16 | Workout workout = new Workout(); 17 | 18 | measurement.reset();; 19 | workout.onMeasured(measurement); 20 | assertEquals(Integer.valueOf(0), workout.duration.get()); 21 | assertEquals(Integer.valueOf(0), workout.distance.get()); 22 | assertEquals(Integer.valueOf(0), workout.strokes.get()); 23 | assertEquals(Integer.valueOf(0), workout.energy.get()); 24 | 25 | measurement.setDuration(0); 26 | measurement.setDistance(1); 27 | measurement.setStrokes(1); 28 | measurement.setEnergy(1); 29 | workout.onMeasured(measurement); 30 | assertEquals(Integer.valueOf(0), workout.duration.get()); 31 | assertEquals(Integer.valueOf(1), workout.distance.get()); 32 | assertEquals(Integer.valueOf(1), workout.strokes.get()); 33 | assertEquals(Integer.valueOf(1), workout.energy.get()); 34 | 35 | measurement.setDuration(1); 36 | measurement.setDistance(2); 37 | measurement.setStrokes(2); 38 | measurement.setEnergy(2); 39 | workout.onMeasured(measurement); 40 | assertEquals(Integer.valueOf(1), workout.duration.get()); 41 | assertEquals(Integer.valueOf(2), workout.distance.get()); 42 | assertEquals(Integer.valueOf(2), workout.strokes.get()); 43 | assertEquals(Integer.valueOf(2), workout.energy.get()); 44 | 45 | measurement.setDuration(1); 46 | measurement.setDistance(3); 47 | measurement.setStrokes(3); 48 | measurement.setEnergy(3); 49 | workout.onMeasured(measurement); 50 | assertEquals(Integer.valueOf(1), workout.duration.get()); 51 | assertEquals(Integer.valueOf(3), workout.distance.get()); 52 | assertEquals(Integer.valueOf(3), workout.strokes.get()); 53 | assertEquals(Integer.valueOf(3), workout.energy.get()); 54 | 55 | measurement.setDuration(2); 56 | measurement.setDistance(4); 57 | measurement.setStrokes(4); 58 | measurement.setEnergy(4); 59 | workout.onMeasured(measurement); 60 | assertEquals(Integer.valueOf(2), workout.duration.get()); 61 | assertEquals(Integer.valueOf(4), workout.distance.get()); 62 | assertEquals(Integer.valueOf(4), workout.strokes.get()); 63 | assertEquals(Integer.valueOf(4), workout.energy.get()); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/io/ProgramImport.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.io; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.os.Handler; 6 | import android.util.Log; 7 | import android.util.Pair; 8 | import android.widget.Toast; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.io.Reader; 14 | import java.text.ParseException; 15 | import java.util.List; 16 | 17 | import svenmeier.coxswain.Coxswain; 18 | import svenmeier.coxswain.Gym; 19 | import svenmeier.coxswain.R; 20 | import svenmeier.coxswain.garmin.TCX2Workout; 21 | import svenmeier.coxswain.gym.Program; 22 | import svenmeier.coxswain.gym.Snapshot; 23 | import svenmeier.coxswain.gym.Workout; 24 | 25 | /** 26 | */ 27 | public class ProgramImport implements Import { 28 | 29 | private Context context; 30 | 31 | private Handler handler = new Handler(); 32 | 33 | private final Gym gym; 34 | 35 | public ProgramImport(Context context) { 36 | this.context = context; 37 | 38 | this.handler = new Handler(); 39 | 40 | this.gym = Gym.instance(context); 41 | } 42 | 43 | public void start(Uri uri) { 44 | new Reading(uri); 45 | } 46 | 47 | private class Reading implements Runnable { 48 | 49 | private final Uri uri; 50 | 51 | public Reading(Uri uri) { 52 | this.uri = uri; 53 | 54 | new Thread(this).start(); 55 | } 56 | 57 | @Override 58 | public void run() { 59 | toast(context.getString(R.string.program_import_starting)); 60 | 61 | try { 62 | write(); 63 | } catch (Exception e) { 64 | Log.e(Coxswain.TAG, "export failed", e); 65 | toast(context.getString(R.string.program_import_failed)); 66 | return; 67 | } 68 | 69 | toast(String.format(context.getString(R.string.program_import_finished))); 70 | } 71 | 72 | private void write() throws IOException, ParseException { 73 | 74 | Program program; 75 | 76 | Reader reader = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(uri))); 77 | try { 78 | program = new Json2Program(reader).program(); 79 | } finally { 80 | reader.close(); 81 | } 82 | 83 | gym.mergeProgram(program); 84 | } 85 | } 86 | 87 | private void toast(final String text) { 88 | handler.post(new Runnable() { 89 | @Override 90 | public void run() { 91 | Toast.makeText(context, text, Toast.LENGTH_LONG).show(); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/TcxShareExport.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.Manifest; 4 | import android.app.PendingIntent; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentSender; 8 | import android.net.Uri; 9 | import android.os.Build; 10 | import android.os.Environment; 11 | import android.os.Handler; 12 | import android.util.Log; 13 | import android.widget.Toast; 14 | 15 | import java.io.BufferedWriter; 16 | import java.io.File; 17 | import java.io.FileWriter; 18 | import java.io.IOException; 19 | import java.io.Writer; 20 | import java.text.SimpleDateFormat; 21 | 22 | import propoid.db.Match; 23 | import propoid.util.content.Preference; 24 | import svenmeier.coxswain.Coxswain; 25 | import svenmeier.coxswain.Gym; 26 | import svenmeier.coxswain.R; 27 | import svenmeier.coxswain.gym.Snapshot; 28 | import svenmeier.coxswain.gym.Workout; 29 | import svenmeier.coxswain.io.Export; 30 | import svenmeier.coxswain.util.PermissionBlock; 31 | 32 | import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 33 | 34 | /** 35 | */ 36 | public class TcxShareExport extends TcxExport { 37 | 38 | public TcxShareExport(Context context) { 39 | super(context); 40 | } 41 | 42 | @Override 43 | protected void onWritten(File file) { 44 | Intent shareIntent = new Intent(Intent.ACTION_SEND); 45 | shareIntent.setType("text/xml"); 46 | shareIntent.putExtra(Intent.EXTRA_SUBJECT, file.getName()); 47 | setFile(context, file, shareIntent); 48 | 49 | if (automatic) { 50 | String sharePackage = Preference.getString(context, R.string.preference_export_tcx_share_package).get(); 51 | if (sharePackage != null) { 52 | shareIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); 53 | shareIntent.setPackage(sharePackage); 54 | 55 | context.startActivity(shareIntent); 56 | return; 57 | } 58 | } 59 | 60 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { 61 | IntentSender sender = PendingIntent.getBroadcast(context, 0, ShareReceiver.newIntent(context), PendingIntent.FLAG_UPDATE_CURRENT).getIntentSender(); 62 | 63 | Intent chooserIntent = Intent.createChooser(shareIntent, context.getString(R.string.garmin_export), sender); 64 | chooserIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); 65 | context.startActivity(chooserIntent); 66 | } else { 67 | context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.garmin_export))); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/garmin/TcxImport.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import android.content.Context; 4 | import android.net.Uri; 5 | import android.os.Handler; 6 | import android.util.Log; 7 | import android.util.Pair; 8 | import android.widget.Toast; 9 | 10 | import java.io.BufferedReader; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.io.Reader; 14 | import java.text.ParseException; 15 | import java.util.List; 16 | 17 | import svenmeier.coxswain.Coxswain; 18 | import svenmeier.coxswain.Gym; 19 | import svenmeier.coxswain.R; 20 | import svenmeier.coxswain.gym.Snapshot; 21 | import svenmeier.coxswain.gym.Workout; 22 | import svenmeier.coxswain.io.Import; 23 | 24 | /** 25 | */ 26 | public class TcxImport implements Import { 27 | 28 | private Context context; 29 | 30 | private Handler handler = new Handler(); 31 | 32 | private final Gym gym; 33 | 34 | public TcxImport(Context context) { 35 | this.context = context; 36 | 37 | this.handler = new Handler(); 38 | 39 | this.gym = Gym.instance(context); 40 | } 41 | 42 | public void start(Uri uri) { 43 | new Reading(uri); 44 | } 45 | 46 | private class Reading implements Runnable { 47 | 48 | private final Uri uri; 49 | 50 | public Reading(Uri uri) { 51 | this.uri = uri; 52 | 53 | new Thread(this).start(); 54 | } 55 | 56 | @Override 57 | public void run() { 58 | toast(context.getString(R.string.garmin_import_starting)); 59 | 60 | try { 61 | write(); 62 | } catch (Exception e) { 63 | Log.e(Coxswain.TAG, "export failed", e); 64 | toast(context.getString(R.string.garmin_import_failed)); 65 | return; 66 | } 67 | 68 | toast(String.format(context.getString(R.string.garmin_import_finished))); 69 | } 70 | 71 | private void write() throws IOException, ParseException { 72 | 73 | TCX2Workout tcx2Workout; 74 | 75 | Reader reader = new BufferedReader(new InputStreamReader(context.getContentResolver().openInputStream(uri))); 76 | try { 77 | tcx2Workout = new TCX2Workout(reader); 78 | tcx2Workout.workout(); 79 | } finally { 80 | reader.close(); 81 | } 82 | 83 | gym.add(tcx2Workout.getProgramName(), tcx2Workout.getWorkout(), tcx2Workout.getSnapshots()); 84 | } 85 | } 86 | 87 | private void toast(final String text) { 88 | handler.post(new Runnable() { 89 | @Override 90 | public void run() { 91 | Toast.makeText(context, text, Toast.LENGTH_LONG).show(); 92 | } 93 | }); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/PowerCalculator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower.wired; 17 | 18 | import java.util.LinkedList; 19 | 20 | import svenmeier.coxswain.gym.Measurement; 21 | import svenmeier.coxswain.rower.ITrace; 22 | 23 | public class PowerCalculator { 24 | 25 | private final ITrace trace; 26 | ; 27 | private LinkedList powerHistory = new LinkedList<>(); 28 | 29 | private int maxPower; 30 | 31 | public PowerCalculator(ITrace trace) { 32 | this.trace = trace; 33 | } 34 | 35 | public void clear(Measurement measurement) { 36 | maxPower = 0; 37 | powerHistory.clear(); 38 | 39 | measurement.setPower(0); 40 | } 41 | 42 | public void power(Measurement measurement, int power) { 43 | // waterrower might report different values during single stroke 44 | maxPower = Math.max(maxPower, power); 45 | } 46 | 47 | public void strokeStart(Measurement measurement, long when) { 48 | // stroke has finished 49 | if (maxPower > 0) { 50 | addHistory(maxPower); 51 | maxPower = 0; 52 | } 53 | 54 | int meanPower = getHistoryMean(); 55 | measurement.setPower(meanPower); 56 | trace.comment("power mean of " + powerHistory + " + is " + meanPower); 57 | } 58 | 59 | private int getHistoryMean() { 60 | if (powerHistory.isEmpty()) { 61 | return 0; 62 | } 63 | 64 | int sum = 0; 65 | for (int i = 0; i < powerHistory.size(); i++) { 66 | sum += powerHistory.get(i); 67 | } 68 | 69 | return sum / powerHistory.size(); 70 | } 71 | 72 | private void addHistory(int power) { 73 | powerHistory.addLast(power); 74 | 75 | if (powerHistory.size() > 6) { 76 | powerHistory.removeFirst(); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/FileTrace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower; 17 | 18 | import android.content.Context; 19 | import android.content.Intent; 20 | import android.net.Uri; 21 | import android.os.Environment; 22 | 23 | import java.io.BufferedWriter; 24 | import java.io.File; 25 | import java.io.FileWriter; 26 | import java.io.IOException; 27 | 28 | import svenmeier.coxswain.Coxswain; 29 | 30 | public class FileTrace implements ITrace { 31 | 32 | public static final String TRACE_FILE = "waterrower.trace"; 33 | 34 | private final BufferedWriter writer; 35 | 36 | public FileTrace(Context context) throws IOException { 37 | File dir = Coxswain.getExternalFilesDir(context); 38 | dir.mkdirs(); 39 | dir.setReadable(true, false); 40 | 41 | File file = new File(dir, TRACE_FILE); 42 | 43 | writer = new BufferedWriter(new FileWriter(file)); 44 | 45 | // input media so file can be found via MTB 46 | context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file))); 47 | 48 | } 49 | 50 | @Override 51 | public void comment(CharSequence string) { 52 | trace('#', string); 53 | } 54 | 55 | @Override 56 | public void onOutput(CharSequence string) { 57 | trace('>', string); 58 | } 59 | 60 | @Override 61 | public void onInput(CharSequence string) { 62 | trace('<', string); 63 | } 64 | 65 | private void trace(char prefix, CharSequence message) { 66 | try { 67 | writer.append(Long.toString(System.currentTimeMillis())); 68 | writer.append(prefix); 69 | writer.append(' '); 70 | writer.append(message); 71 | writer.append('\n'); 72 | writer.flush(); 73 | } catch (IOException ignore) { 74 | } 75 | } 76 | 77 | @Override 78 | public void close() { 79 | try { 80 | writer.close(); 81 | } catch (IOException ignore) { 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "co.riiid.gradle" version "0.4.2" 3 | } 4 | 5 | apply plugin: 'com.android.application' 6 | 7 | if (project.hasProperty("coxswain-private") && new File(project.property("coxswain-private")).exists()) { 8 | apply from: project.property("coxswain-private") 9 | } 10 | 11 | android { 12 | compileSdkVersion 30 13 | 14 | defaultConfig { 15 | applicationId "svenmeier.coxswain" 16 | minSdkVersion 16 17 | targetSdkVersion 29 18 | 19 | versionCode Integer.parseInt(project.property("coxswain-versionCode")) 20 | versionName project.property("coxswain-versionName") 21 | } 22 | buildTypes { 23 | debug { 24 | debuggable true 25 | } 26 | release { 27 | debuggable false 28 | minifyEnabled false 29 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 30 | } 31 | } 32 | productFlavors { 33 | } 34 | } 35 | 36 | ext { 37 | supportLibraryVersion = '1.0.0' 38 | playFitServicesVersion = '20.0.0' 39 | playAuthServicesVersion = '19.0.0' 40 | } 41 | 42 | dependencies { 43 | implementation files('aars/antpluginlib_3-8-0.aar') 44 | implementation "androidx.gridlayout:gridlayout:$supportLibraryVersion" 45 | implementation "androidx.legacy:legacy-support-v13:$supportLibraryVersion" 46 | implementation "androidx.legacy:legacy-preference-v14:$supportLibraryVersion" 47 | implementation 'com.google.android.material:material:1.2.0' 48 | implementation 'androidx.constraintlayout:constraintlayout:2.0.0' 49 | implementation "com.google.android.gms:play-services-fitness:$playFitServicesVersion" 50 | implementation "com.google.android.gms:play-services-auth:$playAuthServicesVersion" 51 | implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' 52 | testImplementation 'junit:junit:4.13' 53 | testImplementation('org.robolectric:robolectric:4.3.1') { 54 | exclude group: 'commons-logging', module: 'commons-logging' 55 | exclude group: 'org.apache.httpcomponents', module: 'httpclient' 56 | } 57 | implementation project(':propoid-core') 58 | implementation project(':propoid-db') 59 | implementation project(':propoid-ui') 60 | implementation project(':propoid-util') 61 | } 62 | 63 | github { 64 | owner = 'svenmeier' 65 | repo = 'coxswain' 66 | tagName = project.property("coxswain-versionName") 67 | targetCommitish = 'master' 68 | name = 'v' + project.property("coxswain-versionName") 69 | body = new File("changelog").getText("UTF-8") 70 | assets = [ 71 | 'app/build/outputs/apk/release/app-release.apk', 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/CompactService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain; 17 | 18 | import android.app.job.JobInfo; 19 | import android.app.job.JobParameters; 20 | import android.app.job.JobScheduler; 21 | import android.app.job.JobService; 22 | import android.content.ComponentName; 23 | import android.content.Context; 24 | import android.os.Build; 25 | import androidx.annotation.RequiresApi; 26 | 27 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 28 | public class CompactService extends JobService { 29 | 30 | private static final long PERIOD = 4 * 60 * 60 * 1000l; 31 | 32 | private static final int WORKOUT_COUNT = 10; 33 | 34 | private Gym gym; 35 | 36 | @Override 37 | public void onCreate() { 38 | gym = Gym.instance(this); 39 | } 40 | 41 | @Override 42 | public boolean onStartJob(JobParameters jobParameters) { 43 | 44 | new Thread(new Compacter(jobParameters)).start(); 45 | 46 | // asynchronous 47 | return true; 48 | } 49 | 50 | private class Compacter implements Runnable { 51 | 52 | private final JobParameters parameters; 53 | 54 | public Compacter(JobParameters jobParameters) { 55 | this.parameters = jobParameters; 56 | } 57 | 58 | @Override 59 | public void run() { 60 | gym.compact(WORKOUT_COUNT); 61 | 62 | jobFinished(parameters, false); 63 | } 64 | } 65 | 66 | @Override 67 | public boolean onStopJob(JobParameters jobParameters) { 68 | return false; 69 | } 70 | 71 | public static void setup(Context context) { 72 | JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); 73 | 74 | JobInfo info = new JobInfo.Builder(42, 75 | new ComponentName(context, CompactService.class)) 76 | .setRequiresDeviceIdle(true) 77 | .setRequiresCharging(true) 78 | .setPeriodic(PERIOD) 79 | .build(); 80 | 81 | jobScheduler.schedule(info); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/gym/Snapshot.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.gym; 17 | 18 | import propoid.core.Property; 19 | import propoid.core.Propoid; 20 | import svenmeier.coxswain.rower.Rower; 21 | 22 | /** 23 | */ 24 | public class Snapshot extends Propoid { 25 | 26 | public final Property workout = property(); 27 | 28 | public final Property difficulty = property(); 29 | 30 | /** 31 | * meters 32 | */ 33 | public final Property distance = property(); 34 | 35 | public final Property strokes = property(); 36 | 37 | /** 38 | * kilo calories 39 | */ 40 | public final Property energy = property(); 41 | 42 | /** 43 | * centimeters per second 44 | */ 45 | public final Property speed = property(); 46 | 47 | /** 48 | * beats per minute 49 | */ 50 | public final Property pulse = property(); 51 | 52 | /** 53 | * strokes per minute 54 | */ 55 | public final Property strokeRate = property(); 56 | 57 | public final Property strokeRatio = property(); 58 | 59 | /** 60 | * watts 61 | */ 62 | public final Property power = property(); 63 | 64 | public Snapshot() { 65 | difficulty.set(Difficulty.NONE); 66 | distance.set(0); 67 | strokes.set(0); 68 | speed.set(0); 69 | energy.set(0); 70 | pulse.set(0); 71 | strokeRate.set(0); 72 | strokeRatio.set(0); 73 | power.set(0); 74 | } 75 | 76 | public Snapshot(Difficulty aDifficulty, Measurement measurement) { 77 | difficulty.set(aDifficulty); 78 | distance.set(measurement.getDistance()); 79 | strokes.set(measurement.getStrokes()); 80 | energy.set(measurement.getEnergy()); 81 | speed.set(measurement.getSpeed()); 82 | pulse.set(measurement.getPulse()); 83 | strokeRate.set(measurement.getStrokeRate()); 84 | strokeRatio.set(measurement.getStrokeRatio()); 85 | power.set(measurement.getPower()); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /app/src/test/java/svenmeier/coxswain/garmin/TCX2WorkoutTest.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.garmin; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.robolectric.RobolectricTestRunner; 6 | import org.robolectric.annotation.Config; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStreamReader; 10 | import java.io.Reader; 11 | import java.text.ParseException; 12 | import java.util.List; 13 | 14 | import svenmeier.coxswain.gym.Snapshot; 15 | import svenmeier.coxswain.gym.Workout; 16 | 17 | import static org.junit.Assert.assertEquals; 18 | 19 | /** 20 | * Test for {@link TCX2Workout}. 21 | */ 22 | @RunWith(RobolectricTestRunner.class) 23 | @Config(constants = svenmeier.coxswain.BuildConfig.class) 24 | public class TCX2WorkoutTest { 25 | 26 | private static final long Mon_Jun_15_2015 = 1434326400000l; 27 | 28 | @Test 29 | public void snapshots() throws IOException, ParseException { 30 | Reader reader = new InputStreamReader(getClass().getResourceAsStream("/snapshots.tcx")); 31 | 32 | TCX2Workout to = new TCX2Workout(reader).workout(); 33 | 34 | Workout workout = to.getWorkout(); 35 | 36 | assertEquals(Mon_Jun_15_2015 + (60 * 1000), (long)workout.start.get()); 37 | assertEquals(3, (int)workout.duration.get()); 38 | assertEquals(6, (int)workout.distance.get()); 39 | assertEquals(2, (int)workout.strokes.get()); 40 | assertEquals(3, (int)workout.energy.get()); 41 | 42 | List snapshots = to.getSnapshots(); 43 | 44 | assertEquals(3, snapshots.size()); 45 | 46 | Snapshot snapshot = snapshots.get(0); 47 | assertEquals(4_50, (int)snapshot.speed.get()); 48 | assertEquals(80, (int)snapshot.pulse.get()); 49 | assertEquals(25, (int)snapshot.strokeRate.get()); 50 | assertEquals(2, (int)snapshot.distance.get()); 51 | assertEquals(0, (int)snapshot.strokes.get()); // trackpoints do not have steps 52 | assertEquals(0, (int)snapshot.power.get()); 53 | 54 | snapshot = snapshots.get(1); 55 | assertEquals(4_51, (int)snapshot.speed.get()); 56 | assertEquals(81, (int)snapshot.pulse.get()); 57 | assertEquals(26, (int)snapshot.strokeRate.get()); 58 | assertEquals(4, (int)snapshot.distance.get()); 59 | assertEquals(0, (int)snapshot.strokes.get()); // trackpoints do not have steps 60 | assertEquals(1, (int)snapshot.power.get()); 61 | 62 | snapshot = snapshots.get(2); 63 | assertEquals(4_52, (int)snapshot.speed.get()); 64 | assertEquals(82, (int)snapshot.pulse.get()); 65 | assertEquals(27, (int)snapshot.strokeRate.get()); 66 | assertEquals(6, (int)snapshot.distance.get()); 67 | assertEquals(0, (int)snapshot.strokes.get()); // trackpoints do not have steps 68 | assertEquals(2, (int)snapshot.power.get()); 69 | 70 | assertEquals("Test Program", to.getProgramName()); 71 | } 72 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 18 | 19 | 27 | 28 | 33 | 34 | 41 | 47 | 53 | 54 | 55 | 56 | 62 | 63 | 64 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/sensors/SensorsHeart.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.sensors; 2 | 3 | import android.Manifest; 4 | import android.content.Context; 5 | import android.hardware.Sensor; 6 | import android.hardware.SensorEvent; 7 | import android.hardware.SensorEventListener; 8 | import android.hardware.SensorManager; 9 | import android.widget.Toast; 10 | 11 | import svenmeier.coxswain.Heart; 12 | import svenmeier.coxswain.R; 13 | import svenmeier.coxswain.gym.Measurement; 14 | import svenmeier.coxswain.util.PermissionBlock; 15 | 16 | /** 17 | * {@link Heart} using the device's sensor. 18 | */ 19 | public class SensorsHeart extends Heart { 20 | 21 | private static final int TYPE_HEART_RATE_LEGACY = 65562; 22 | 23 | private Connection connection; 24 | 25 | public SensorsHeart(Context context, Measurement measurement, Callback callback) { 26 | super(context, measurement, callback); 27 | 28 | connection = new Connection(context); 29 | connection.open(); 30 | } 31 | 32 | @Override 33 | public void destroy() { 34 | if (connection != null) { 35 | connection.close(); 36 | connection = null; 37 | } 38 | } 39 | 40 | private class Connection extends PermissionBlock implements SensorEventListener { 41 | 42 | private Sensor sensor; 43 | 44 | public Connection(Context context) { 45 | super(context); 46 | } 47 | 48 | public void open() { 49 | acquirePermissions(Manifest.permission.BODY_SENSORS); 50 | } 51 | 52 | @Override 53 | protected void onPermissionsApproved() { 54 | SensorManager manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); 55 | 56 | sensor = manager.getDefaultSensor(Sensor.TYPE_HEART_RATE); 57 | if (sensor == null) { 58 | sensor = manager.getDefaultSensor(TYPE_HEART_RATE_LEGACY); 59 | } 60 | if (sensor == null) { 61 | Toast.makeText(context, R.string.sensors_heart_not_found, Toast.LENGTH_LONG).show(); 62 | close(); 63 | return; 64 | } 65 | 66 | Toast.makeText(context, R.string.sensors_heart_reading, Toast.LENGTH_LONG).show(); 67 | manager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL); 68 | } 69 | 70 | @Override 71 | protected void onRejected() { 72 | Toast.makeText(context, R.string.sensors_heart_rejected, Toast.LENGTH_LONG).show(); 73 | } 74 | 75 | public void close() { 76 | abortPermissions(); 77 | 78 | if (sensor != null) { 79 | SensorManager manager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); 80 | 81 | manager.unregisterListener(this); 82 | } 83 | } 84 | 85 | @Override 86 | public void onSensorChanged(SensorEvent event) { 87 | if (event.values != null && event.values.length > 0) { 88 | onHeartRate(Math.round(event.values[0])); 89 | } 90 | } 91 | 92 | @Override 93 | public void onAccuracyChanged(Sensor sensor, int i) { 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/ValueBinding.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view; 2 | 3 | import android.content.Context; 4 | 5 | import propoid.util.content.Preference; 6 | import svenmeier.coxswain.R; 7 | 8 | /** 9 | */ 10 | public enum ValueBinding { 11 | 12 | DURATION(R.string.duration_label, R.string.duration_pattern), 13 | DISTANCE(R.string.distance_label, R.string.distance_pattern), 14 | STROKES(R.string.strokes_label, R.string.strokes_pattern), 15 | ENERGY(R.string.energy_label, R.string.energy_pattern), 16 | SPEED(R.string.speed_label, R.string.speed_pattern), 17 | AVERAGE_SPEED(R.string.average_speed_label, R.string.speed_pattern), 18 | PULSE(R.string.pulse_label, R.string.pulse_pattern), 19 | STROKE_RATE(R.string.strokeRate_label, R.string.strokeRate_pattern), 20 | STROKE_RATIO(R.string.strokeRatio_label, R.string.strokeRatio_pattern), 21 | POWER(R.string.power_label, R.string.power_pattern), 22 | TIME(R.string.time_label, R.string.time_pattern), 23 | SPLIT(R.string.split_label, R.string.split_pattern), 24 | AVERAGE_SPLIT(R.string.average_split_label, R.string.average_split_pattern), 25 | DELTA_DURATION(R.string.delta_duration_label, R.string.delta_duration_pattern), 26 | DELTA_DISTANCE(R.string.delta_distance_label, R.string.delta_distance_pattern), 27 | NONE(R.string.none_label, R.string.none_pattern); 28 | 29 | public final int label; 30 | public final int pattern; 31 | 32 | ValueBinding(int label, int pattern) { 33 | this.label = label; 34 | this.pattern = pattern; 35 | } 36 | 37 | public String format(Context context, int value, boolean arabic) { 38 | return format(context, value, arabic, false); 39 | } 40 | 41 | public String format(Context context, int value, boolean arabic, boolean signed) { 42 | StringBuilder text = new StringBuilder(); 43 | 44 | int digits = Math.abs(value); 45 | String pattern = context.getString(this.pattern); 46 | 47 | for (int c = pattern.length() - 1; c >= 0; c--) { 48 | char character = pattern.charAt(c); 49 | 50 | if ('0' == character) { 51 | // decimal 52 | text.append(toChar(digits % 10, arabic)); 53 | 54 | digits /= 10; 55 | } else if ('6' == character) { 56 | // minutes or hours 57 | text.append(toChar(digits % 6, arabic)); 58 | 59 | digits /= 6; 60 | } else if ('F' == character) { 61 | // hexadecimal 62 | text.append(Integer.toHexString(digits % 0xF)); 63 | 64 | digits /= 0xF; 65 | } else if ('-' == character){ 66 | if (value < 0) { 67 | text.append("-"); 68 | } else if (signed) { 69 | text.append("+"); 70 | } 71 | } else { 72 | text.append(character); 73 | } 74 | } 75 | 76 | text.reverse(); 77 | 78 | return text.toString(); 79 | } 80 | 81 | private char toChar(int digit, boolean arabic) { 82 | if (arabic) { 83 | return (char)(0x660 + digit); 84 | } else { 85 | return (char)('0' + digit); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/rower/wired/NumberField.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Sven Meier 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package svenmeier.coxswain.rower.wired; 17 | 18 | import svenmeier.coxswain.gym.Measurement; 19 | 20 | /** 21 | */ 22 | public abstract class NumberField extends Field { 23 | 24 | public static final int SINGLE_BYTE = 1; 25 | public static final int DOUBLE_BYTE = 2; 26 | public static final int TRIPLE_BYTE = 3; 27 | 28 | protected static final int CODEPOINT_0 = 48; 29 | protected static final int CODEPOINT_A = 65; 30 | 31 | /** 32 | * @param address memory address 33 | * @param size data size SINGLE_BYTE, DOUBLE_BYTE or TRIPLE_BYTE 34 | */ 35 | NumberField(int address, int size) { 36 | String ach = toAscii(address, 3, 16); 37 | 38 | switch (size) { 39 | case 1: 40 | this.request = "IRS" + ach; 41 | this.response = "IDS" + ach; 42 | break; 43 | case 2: 44 | this.request = "IRD" + ach; 45 | this.response = "IDD" + ach; 46 | break; 47 | case 3: 48 | this.request = "IRT" + ach; 49 | this.response = "IDT" + ach; 50 | break; 51 | default: 52 | throw new IllegalArgumentException("unkown size " + size); 53 | } 54 | } 55 | 56 | @Override 57 | protected void onInput(String message, Measurement measurement) { 58 | onUpdate(fromAscii(message, response.length()), measurement); 59 | } 60 | 61 | protected abstract void onUpdate(int value, Measurement measurement); 62 | 63 | protected int fromAscii(String data, int start) { 64 | int total = 0; 65 | 66 | for (int c = start; c < data.length(); c++) { 67 | total *= 16; 68 | 69 | int codepoint = (int)data.charAt(c); 70 | int digit = codepoint - CODEPOINT_0; 71 | if (digit > 9) { 72 | digit = 10 + (codepoint - CODEPOINT_A); 73 | } 74 | 75 | total += digit; 76 | } 77 | 78 | return total; 79 | } 80 | 81 | protected String toAscii(int value, int length, int base) { 82 | String s = Integer.toString(value, base).toUpperCase(); 83 | 84 | while (s.length() < length) { 85 | s = '0' + s; 86 | } 87 | 88 | return s; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /changelog: -------------------------------------------------------------------------------- 1 | 8.8 2 | - fixed energy adjustment 3 | - fixed external storage on Android 11 4 | 5 | 8.6 6 | - show results after workout end 7 | - possible fix for export to Google Fit 8 | - speak seconds 9 | - adjust distance when adjusting speed 10 | - French translation 11 | 12 | 8.4 13 | - fixed open end to continue rowing 14 | 15 | 8.3 16 | - two additional difficulties for segments 17 | - show/hide snapshots with rest difficulty 18 | - added workout information table 19 | - improved rower reset and trace 20 | 21 | 8.2 22 | - improved reset detection 23 | - show toast when rower could not be reset 24 | 25 | 8.1 26 | - use new gatt api where available 27 | - fixed possible error while reading values via bluetooth 28 | 29 | 7.8 30 | - fixed Google Fit login 31 | 32 | 7.7 33 | - allow export of application log 34 | 35 | 7.6 36 | - let speed, strokerate and power drop to 0 when idle 37 | - improved power calculation 38 | 39 | 7.5 40 | - updated ANT+ library for Android 10 41 | - filter workouts for selected program 42 | 43 | 7.4 44 | - fixed file access on Android Q 45 | 46 | 7.3 47 | - notify on low battery of bluetooth devices 48 | - sort programs by name 49 | 50 | 7.2 51 | - fixed premature ending of workout with 1.x revision of ComModule 52 | 53 | 7.1 54 | - reimplemented Google Fit access 55 | - read power in tcx import 56 | 57 | 7.0 58 | - write trace for bluetooth rower 59 | - fixed invalid reading via bluetooth at 60 minutes 60 | 61 | 6.9 62 | - fixed measurement of durations longer than 60 minutes 63 | 64 | 6.8 65 | - reset rower via bluetooth 66 | - fixed values of snapshot axis 67 | 68 | 6.7 69 | - another fix for premature ending of workout :/ 70 | 71 | 6.6 72 | - duplicate programs 73 | - duplicate segments 74 | - reorder segments 75 | 76 | 6.5 77 | - fixed Google Fit export 78 | 79 | 6.4 80 | - improved (broken) Google Fit export 81 | - remember share intent for automatic export 82 | 83 | 6.3 84 | - fixed premature ending of rowing when adjusting energy 85 | 86 | 6.2 87 | - support bluetooth rower connection 88 | 89 | 6.1 90 | - support bluetooth device selection 91 | - revamped charts 92 | 93 | 6.0 94 | - support picture-in-picture 95 | 96 | 5.9 97 | - export with artificial locations, by default River Thames 98 | 99 | 5.8 100 | - fixed calendar export 101 | 102 | 5.7 103 | - don't repeat segment limit when pausing 104 | - record power and export to TCX 105 | 106 | 5.6 107 | - fixed performance regression 108 | 109 | 5.3 110 | - support external storage location 111 | - improved number display 112 | - arabic numerals 113 | - allow 5 and 15 strokes target 114 | 115 | 5.2 116 | - allow explicit connect to a device as workaround for Sony devices 117 | 118 | 5.1 119 | - fixed crash when selecting workout as challenge 120 | - fixed bug increasing database size excessively 121 | - remove unnecessary data from database 122 | -------------------------------------------------------------------------------- /app/src/main/java/svenmeier/coxswain/view/preference/RingtonePreference.java: -------------------------------------------------------------------------------- 1 | package svenmeier.coxswain.view.preference; 2 | 3 | import android.content.Context; 4 | import android.content.Intent; 5 | import android.content.res.TypedArray; 6 | import android.media.Ringtone; 7 | import android.media.RingtoneManager; 8 | import android.net.Uri; 9 | import android.util.AttributeSet; 10 | 11 | import svenmeier.coxswain.R; 12 | 13 | /** 14 | * An specialization that substitutes the current ringtone into the summary (as ListPreference does it 15 | * too). 16 | * Additionally the support library doesn't support it yet :/. 17 | */ 18 | public class RingtonePreference extends ResultPreference { 19 | 20 | private String defaultValue; 21 | 22 | public RingtonePreference(Context context, AttributeSet attrs, int defStyleAttr) { 23 | super(context, attrs, defStyleAttr); 24 | 25 | init(context, attrs); 26 | } 27 | 28 | private void init(Context context, AttributeSet attrs) { 29 | 30 | TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.Preference); 31 | 32 | this.defaultValue = array.getString(R.styleable.Preference_android_defaultValue); 33 | } 34 | 35 | public RingtonePreference(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | 38 | init(context, attrs); 39 | } 40 | 41 | public RingtonePreference(Context context) { 42 | super(context); 43 | } 44 | 45 | @Override 46 | public Intent getRequest() { 47 | Intent intent = new Intent(RingtoneManager.ACTION_RINGTONE_PICKER); 48 | intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION); 49 | intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true); 50 | intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true); 51 | intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Uri.parse(defaultValue)); 52 | 53 | String existingValue = getPersistedString(null); 54 | if (existingValue != null) { 55 | intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, Uri.parse(existingValue)); 56 | } 57 | 58 | return intent; 59 | } 60 | 61 | @Override 62 | public void onResult(Intent intent) { 63 | Uri ringtone = null; 64 | if (intent != null) { 65 | ringtone = intent.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI); 66 | } 67 | 68 | String value = null; 69 | if (ringtone != null) { 70 | value = ringtone.toString(); 71 | } 72 | 73 | persistString(value); 74 | notifyChanged(); 75 | } 76 | 77 | @Override 78 | public CharSequence getSummary() { 79 | CharSequence summary = super.getSummary(); 80 | if (summary == null) { 81 | return summary; 82 | } 83 | 84 | String string = getPersistedString(null); 85 | 86 | if (string == null || string.isEmpty()) { 87 | string = getContext().getString(R.string.preference_audio_none); 88 | } else { 89 | Ringtone ringtone = RingtoneManager.getRingtone(getContext(), Uri.parse(string)); 90 | if (ringtone != null) { 91 | string = ringtone.getTitle(getContext()); 92 | } 93 | } 94 | 95 | return String.format(summary.toString(), string); 96 | } 97 | } -------------------------------------------------------------------------------- /app/src/test/resources/snapshots.tcx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2015-06-15T00:01:00.000Z 6 | 7 | 2 8 | 6 9 | 3 10 | Active 11 | Manual 12 | 13 | 14 | 15 | 16 | 0.0 17 | 0.0 18 | 19 | 2 20 | 21 | 80 22 | 23 | 25 24 | 25 | 26 | 4.5 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 0.0 35 | 0.0 36 | 37 | 4 38 | 39 | 81 40 | 41 | 26 42 | 43 | 44 | 4.51 45 | 1 46 | 47 | 48 | 49 | 50 | 51 | 52 | 0.0 53 | 0.0 54 | 55 | 6 56 | 57 | 82 58 | 59 | 27 60 | 61 | 62 | 4.52 63 | 2 64 | 65 | 66 | 67 | 68 | 69 | 70 | 2 71 | 72 | 73 | 74 | Created by Coxswain 75 | 76 | 77 | Test Program 78 | 79 | 80 | 81 | 82 | --------------------------------------------------------------------------------