RaceCommittee App
Introduction
The RaceCommittee App is an Android 3.2 Tablet application. This document serves as the main information hub about the App and how to develop for it.
- See the OnBoarding Information on how to setup your local environment for building the app
- See Mobile Development for general tipps on mobile development.
- See Server Environment on how the server's build environment is configured for building the RaceCommittee App.
User Guide
Have a look at the following user guides to get an idea how to work with the RaceCommittee App.
Features
A Feature List collected in September 2013 at the Testevent in Santander can be seen here: Feature List RaceCommittee App (RaceCommittee Feature List, Sep. 2013, Julian Gimbel) The Feedback a User gave to the Application and the Feedback the user gave to the Applikation and Hardware of SwissTiming can be seen here: Feature List RaceCommittee App (Swiss Timing App and Feedback for SAP RaceCommittee App, Sep. 2013, Julian Gimbel)
Course Updates
The app has several course designer varying in their input method and output:
- By-Name Course Designer: just setting the name of a CourseBase object with no waypoints
- By-Map Course Designer: just setting the name of a CourseBase object with now waypoints
- By-Marks Course Designer: full CourseBase object with waypoints
The CourseBase is attached to a RaceLogCourseDesignChangedEvent. On the server the TrackedRace attached to the race log will forward such events to the TracTracCourseDesignUpdateHandler. If activated this handler will forward the CourseBase object (regardless of whether its has waypoints or not) to TracTrac.
RaceState
The RaceState (and its read-only variation ReadonlyRaceState) should be the only way you query and change the state of race (i.e. the state of a RaceLog). The RaceState analyzes the content of its underlying RaceLog for you and provides a clear interface to several aspects of your race, including its start time, its finished time, the selected course design, etc.
A RaceState always has a RacingProcedure attached to it. Whenever racing procedure type is set by a call to RaceState#setRacingProcedureType the RacingProcedure object will be recreated (even when the type was not changed).
See the code documentation for further details.
Adding a new user interface
When writing a user interface for the state of a RaceLog / race you should use the RaceState interface. Consult the code documentation on how to create a RaceState (or ReadonlyRaceState).
Since your UI should get all updates, you should set a RaceStateEventScheduler on the RaceState. This enables automatic events to be triggered by the RacingProcedure (e.g. when the active flags of the starting sequence have changed). See below on how to implement a RaceStateEventScheduler.
Using a RaceState for your UI you want to leverage the callback mechanisms rather than re-creating one-shot RaceStates and querying their status over and over again. Register your RaceStateChangedListener on the RaceState and your RacingProcedureChangedListener on its RacingProcedure. You should re-register your RacingProcedure listener whenever RaceStateChangedListener#onRacingProcedureChanged is called. Keep in mind that depending on the type of the RacingProcedure there might be additional callback methods available (e.g. for a RRS26 race there is a RRS26ChangedListener ).
The last step is to implement a RacingProcedurePrerequisite.Resolver to support setting a new start time in your UI. See below for a walk-through.
Implementing a RaceStateEventScheduler
A RaceStateEventScheduler is in charge of calling RaceState#processStateEvent whenever a RaceStateEvent passed to RaceStateEventScheduler#scheduleStateEvents is due.
Each RateStateEvent carries a TimePoint. You should set a timer and call RaceState#processStateEvent passing the RaceStateEvent when the timer fires. Keep in mind that RaceState won't do any threading/locking for you. Therefore be careful when calling RaceState#processStateEvent from a background thread, because your UI listener (see above) might be called in this context!
Implementing a RacingProcedurePrerequisite.Resolver
When writing a new user interface for the RaceState your UI code has to be capable of fulfilling possible RacingProcedurePrerequisites. Such a Prerequisite may occur when you're requesting a new start time on the RaceState. The call to RaceState#requestNewStartTime takes a RacingProcedurePrerequisite.Resolver, which will be in charge of fulfilling prerequisites.
The RacingProcedurePrerequisite.Resolver is an asynchronous interface passing you specific prerequisites demanding to be fulfilled. In your implementation of the interface show an appropiate dialog-window or something similar and be sure to call the specific fulfilled method on the passed prerequisite when done. This will trigger the RaceState to check for further prerequisites. When you've fulfilled everything your resolver's onFulfilled method will be called. Afterwards the requested start time will be set.
How does the resolving work
On call to RaceState#requestNewStartTime the RaceState creates an anonymous function (called FulfillmentFunction) to be exected when all prerequistes are fulfilled. This function simply sets the start time on the RaceState as requested.
Next the RaceState asks its RacingProcedure for any prerequisites. It does so by calling RacingProcedure#checkPrerequisitesForStart passing the requested start time and the FulfillmentFunction. The returned RacingProcedurePrerequisite will be resolved by your resolver.
To resolve a prerequisites against a resolver, RacingProcedurePrerequisite#resolve is called passing the resolver. This method implements a simple double dispatch visitor pattern. Specific implementations will call their specific fulfilment method on the resolver (see above). After the resolver has done its work for this prerequisite it will call the specific fulfilled method. This will trigger the base BaseRacingProcedurePrerequisite#fulfilled method, which checks for the next prerequisite on the RacingProcedure (passing down the start time and the fulfillment function) and resolves it against the same resolver object.
This chain continues until the RacingProcedure detects that there are no further prerequisites. It will return a special type of RacingProcedurePrerequisite, the NoMorePrerequisite. On resolve a NoMorePrerequisite executes the fulfillment function and won't call BaseRacingProcedurePrerequisite#fulfilled - effectively breaking the chain of resolving. The start time is set and your resolver RacingProcedurePrerequisite.Resolver#onFulfilled is called.
Adding a new racing procedure
When adding a new racing procedure you should start by basing your work on one of the existing ones. Have a look at the basic countdown racing procedure to see the most simplest. The brave ones base their work on the gate start procedure.
A working racing procedure needs the following:
- A new type in RacingProcedureType
- An implementation of RacingProcedure
- A RacingProcedureConfiguration field in RegattaConfiguration (see below on how to do this)
- App UI - just extend RaceInfoFragmentChooser and you'll see what you need
RaceLog priorities and authors
TODO.
Build and Auto-Update
When build by Maven the resulting APK of the RaceCommittee App will be made available as static content on the server's web page.
The RaceCommittee App is set up as an optional dependency of the bundle com.sap.sailing.www. This way the app will be build before the www-bundle. After the install phase the RaceCommittee App bundle will copy its artifact APK into com.sap.sailing.www/apps. The contents of this folder are packaged into the com.sap.sailing.www plugin, which will be deployed as the server's web page. When build with buildAndUpdateProduct.sh an additional version information file is stored alongside the APK. Version information is taken from the AndroidManifest.xml (android:versionCode).
On synchronizing the connection settings (see administrator's guide) the RaceCommittee App downloads the version file to determine whether it should update itself or not. The file is expected to be found on {SERVER_URL}/apps/{APP_PACKAGE_NAME}.version (e.g. http://ess2020.sapsailing.com/apps/com.sap.sailing.racecommittee.app.version). If the version file is not found, no update will be performed.
See the next section about versioning.
Versioning
The app's version is defined in its AndroidManifest.xml in the versionCode attribute. This version code is used by several components:
- Android OS for standard versioning operations
- Helper-Class PreferenceHelper to determine whether a refresh of the stored preferences is needed
- Auto-Update feature (see above)
This leads to the following situations, in which one should bump the versionCode:
- You feel like you have achieved something remarkable.
- You have added or changed the type of a preference (see below for a walk-through on how to add a new preference option).
- You have made non-backwards-compatible changes to the app<->server interface. This includes changes to serializers/deserializers, changes to servlets,…
- You want to trigger the auto-update because you customized the app for the current event.
Configuration (or Preferences)
Configuration of the app is crucial for the app to function properly. See the administration guide on how to it is done. The main idea is, that all configuration options are editable on the app via the Android standard preferences interfaces. Still most of the configuration should be configurable on the server.
The app fetches its DeviceConfiguration on logon. This overall DeviceConfiguration is merged with configuration that is stored on device. Additionally each regatta can have a specific RegattaConfiguration attached to it. A regata-specific RegattaConfiguration is merged with the overall DeviceConfiguration.
Adding a new configuration option
The following leads you to the process of adding a remote-configurable configuration option. The text assumes you are adding the configuration option to DeviceConfiguration. The process of adding an option to RegattaConfiguration or one of the RacingProcedureConfigurations is very similar.
Estimated time: 1 hour. Main task: copy and paste.
- Device-Local Configuration
- res/xml/preference_xxx.xml add preference (check com.sap.sailing.racecommittee.app.ui.views) and define title and summary in localization files
- res/values/preferences.xml define key string and default value - this default value will be set on first start (or version change!)
- If needed initialize your preference in com.sap.sailing.racecommittee.app.ui.fragments.preference.YourPreferenceFragment (keep in mind: default value will be set automatically)
- If your preference needs to accessed from app code this should be done through the helper class AppPreferences -> create getter (and if needed setter) in AppPreferences
- Exposing the option on the server
- Add a getter for your option to DeviceConfiguration
- Implement the getter and a setter in DeviceConfigurationImpl
- Modify the DeviceConfigurationImpl#clone method (for RegattaConfiguration or one of its items you need to extend the merge method too)
- Teach the com.sap.sailing.racecommittee.app.domain.configuration.PreferencesDeviceConfigurationLoader how to load and store your setting from/to AppPreferences (for RegattaConfiguration you should extend com.sap.sailing.domain.base.configuration.impl.EmptyRegattaConfiguration too)
- Modify the configuration's serializer and deserializer (good news: they are used for persistence, but don't tell anyone)
- Integrate your new option in the GWT UI (Extending the existing DTOs and Dialogs)
If feel the need to add a new category of preferences (i.e. adding a new preference fragment in res/xml/preference_xxx) be sure to modify PreferenceHelper#resetPreferences to incorporate your new screen.