Repository navigation
Android PortalApiClient Gamification
This page describes the gamification APIs exposed by the PortalApiClient framework. While the target audience is coders/developers, it brings better understanding for those who are not too tech-savvy as well.
The package's own README.md and Gamification_QA.md, shipped in
CCFramework-Android-Open, cover the same ground in the shape the
iOS package uses; this page is the reference version.
Gamification turns driving behaviour into a game. The portal awards experience and currency for what the driver does, places drivers on leaderboards, groups them so they compete against people they know, and notifies them when something was won. The framework exposes all of that as read mostly calls; the scoring itself happens on the backend.
The portal has carried gamification twice, and both are still reachable.
| Classic | GamificationZone | |
|---|---|---|
| Nouns | Badge, Challenge, Experience, GCurrency | Player, Game, Round, Leaderboard, Group, Level |
| Facade |
PortalApiClient, Java, callbacks |
PortalApi, Kotlin, suspend
|
| Addressed by | subscription id | player name |
| Use it for | badge walls, one off challenges, a points balance | leaderboards, rivals, repeating rounds |
New work belongs on GamificationZone. The classic calls are documented in section 9 because existing apps still use them.
Player is the driver as the game sees them. A player carries a name, an experience total, a currency balance and an
avatar image id. By default the framework addresses the signed in user, so playerName defaults to the portal user id.
Game is a rule that can be won, for example "no harsh braking for a week". It declares what it pays out in experience and currency, how many times it can be won, and the rewards attached to it.
Round is one run of a game for one player, between startOn and endOn. isObjectiveAchieved says whether the
player won it, and isCompleted is true once the round has an end date.
Leaderboard ranks players by a value over a period. It has a current round of its own, so "this week's table" and
"last week's table" are two rounds of the same leaderboard. GZoneLeaderboard.DEFAULT is the tenant's main table and is
the default argument everywhere a leaderboard name is taken.
Group is a subset of players who are ranked against each other. Public groups are open and discoverable; private
groups are not, and GZonePlayerGroup.DEFAULT is the implicit group a player always belongs to.
Level is an experience band. GZoneExperienceLevel gives the band name, its image, and the experience range that
falls inside it.
| Identifier | How to get it | Used by |
|---|---|---|
| Player name |
PortalApi.getUserId().toString(), the default for every playerName parameter |
all GamificationZone calls |
| Vehicle unit id (pvu) | PortalApi.getSelectedVehicleId() |
classic player, currency, badges |
| Subscription id | PortalApi.getUserSmartDriveSubscriptionId() |
classic challenges, experience |
| Policy number | PortalApiClient.getInstance().selectedVehicle?.policyNumber |
PortalApiClient.getGZoneRounds |
getGZonePlayerSource() returns the player names that belong to the signed in user. Call it once if your tenant gives
a user more than one player.
A gamification screen needs four requests. They are independent, so run them in parallel.
| Call | Returns |
|---|---|
getGZoneLevels() |
List<GZoneExperienceLevel> — level name, image and point range |
getGZonePlayer() |
GZonePlayer — experience points and currency balance |
getGZonePlayerGames() |
List<GZoneGame> — games available to the player |
getGZonePlayerRounds() |
List<GZoneRound> — rounds in progress and completed, with their rewards |
val state = coroutineScope {
val levels = async { PortalApi.getGZoneLevels() }
val player = async { PortalApi.getGZonePlayer() }
val games = async { PortalApi.getGZonePlayerGames() }
val rounds = async { PortalApi.getGZonePlayerRounds() }
GamificationState(
levels = levels.await().result.orEmpty(),
player = player.await().result,
availableGames = games.await().result.orEmpty(),
rounds = rounds.await().result.orEmpty()
)
}| Status | Rule |
|---|---|
| Available | Items returned by getGZonePlayerGames()
|
| In progress | Rounds where isCompleted is false, that is endOn is empty |
| Completed | Rounds where isCompleted is true
|
| Won | Completed rounds where isObjectiveAchieved is true
|
| Rewards |
round.game?.rewards, the GZoneReward list of the completed round |
| Currency | player.currency |
| Current level | The level whose experienceFrom…experienceTo range contains player.experience
|
| Next level / progress | The next range; progress = (experience − current start) ÷ (current end − current start). Above the highest range, the player is at the last level. |
The SDK already derives completion, so the app does not inspect the end date itself:
val inProgress = rounds.filterNot { it.isCompleted }
val won = rounds.filter { it.isCompleted && it.isObjectiveAchieved }
val currentLevel = levels.firstOrNull { level ->
player.experience in level.experienceFrom..level.experienceTo
}Games and rewards are evaluated on the backend. A trip is reflected in game data only after it has been uploaded and processed, and there is no event or push channel for game updates, so the app must request the data again to pick up changes. Each reload returns the latest state; comparing it with the previous load shows what changed, such as a new round, new rewards, or a round that has completed.
Reload all four requests:
- when the gamification screen is opened
- when the user selects another vehicle
- when the app returns to the foreground, if the screen is visible
// Defaults to the signed in user
val player = PortalApi.getGZonePlayer().result
player?.name
player?.experience
player?.currency
player?.imageIdval games = PortalApi.getGZonePlayerGames().result
val rounds = PortalApi.getGZonePlayerRounds(
createdOnFrom = DateTime.now().minusMonths(1),
createdOnTo = DateTime.now()
).result
rounds?.filter { it.isCompleted && it.isObjectiveAchieved }getGZoneRounds is the older, subscription scoped variant. It accepts a date range plus an optional subscriptionId
and userId, and it honours the three caching flags, so it is the one to use when the round history has to be readable
offline.
val cachedRounds = PortalApi.getGZoneRounds(
startDate = DateTime.now().minusWeeks(2),
endDate = DateTime.now(),
useOfflineCache = true
).resultgetGZoneLeaderboardRounds returns the rounds of the leaderboards a player takes part in, rather than the rounds of
their games. Use it to render "week 12 of the season" headers.
A rank is either qualified or unqualified. A player is qualified once they have met whatever minimum the leaderboard requires, usually a number of trips or kilometres. Unqualified players still get a position, reported separately so the app can show "you are 4th, but you need two more trips to count".
// Where the signed in player stands, plus the players immediately around them
val myRank = PortalApi.getGZoneLeaderboardPlayerRank(
startOn = roundStartIso,
rankFrom = 1,
rankTo = 20
).result
myRank?.rank
myRank?.rankPercentage
myRank?.value
myRank?.rivals // the neighbouring rows, already populated
// The table itself
val table = PortalApi.getGZoneLeaderboardRank(
startOn = roundStartIso,
rankFrom = 1,
rankTo = 50
).resultstartOn selects the round and is required. Take it from GZoneLeaderboard.currentRoundStartOn for the live table, or
from an earlier round to show history.
The unqualified equivalents mirror the qualified ones exactly:
val myUnqualified = PortalApi.getGZoneLeaderboardPlayerUnqualifiedRank(startOn = roundStartIso).result
myUnqualified?.qualified
myUnqualified?.unqualifiedRank
myUnqualified?.unqualifiedRankPercentage
val unqualifiedTable = PortalApi.getGZoneLeaderboardUnqualifiedRank(startOn = roundStartIso).resultGroup scoped ranks come in two flavours, and the difference matters:
// Public group: the whole table for that group
val publicGroupTable = PortalApi.getGZoneLeaderboardPublicGroupPlayerRank(
groupName = "CityDrivers",
startOn = DateTime.parse(roundStartIso)
).result
// Private group: only this player's row inside the group
val myRowInGroup = PortalApi.getGZoneLeaderboardPrivateGroupPlayerRank(
groupName = "Family",
startOn = DateTime.parse(roundStartIso)
).resultgetGZonePlayerLeaderboards(playerName) lists the leaderboards a player appears on, which is the right starting point
when a tenant runs more than the default table.
val allPublic = PortalApi.getGZoneAllPublicPlayerGroups().result
PortalApi.addGZonePlayerToPublicGroup(groupName = "CityDrivers", playerName = playerName)
PortalApi.removeGZonePlayerFromPublicGroup(groupName = "CityDrivers", playerName = playerName)
val mine = PortalApi.getGZonePlayerGroups(isPrivate = false).resultJoining and leaving is only defined for public groups. Private group membership is managed in the portal.
getGZoneNotification resolves a gamification notification id the app already holds to the round that produced it. It
resolves an id rather than delivering one, so it is not an event channel, and the Scope reference app handles no
gamification push type. Confirm with the backend team whether a tenant can raise a push for a completed round before
designing against it.
val notification = PortalApi.getGZoneNotification(notificationId).result
val round = notification?.round
val game = round?.gameImages are referenced by id across players, games, levels and rewards. The framework can either download the bytes or hand you a URL for an image loading library:
String url = PortalApiClient.getInstance().getGZoneImageUrl(imageId, 256);
Glide.with(context).load(url).into(imageView);getGZoneImage(imageId, width, callback) downloads the same image as a ResponseBody when you need the bytes.
These calls remain on the Java facade and are addressed by subscription id.
PortalApiClient client = PortalApiClient.getInstance();
// Badges the driver has earned, and the full catalogue of badge rules
client.getBadges(forceRefresh, callback);
client.getAllBadges(forceRefresh, callback);
// Currency balance and experience
client.getCurrencyAmount(subscriptionId, callback);
client.getExperienceData(subscriptionId, callback);
// Challenges, and opting in or out of one
client.getSubscriptionChallenges(subscriptionId, callback);
client.approveParticipation(challengeId, subscriptionId, callback);
client.cancelParticipation(challengeId, subscriptionId, callback);getCurrencyAmount and getExperienceData also have no argument overloads that use the signed in user's SmartDrive
subscription and return the resolved subscription id as a String.
A Challenge reports its state through status, against the constants on the class:
| Constant | Meaning |
|---|---|
STATUS_PARTICIPATING |
the driver is in, and the challenge is running |
STATUS_AVAILABLE |
open to join |
STATUS_PARTICIPATED |
finished, the driver took part |
STATUS_NOT_PARTICIPATED |
finished, the driver did not take part |
Experience is a balance plus the tenant's ExperienceLevel list, where each level carries its point range and four
image sizes.
| Class | Key members |
|---|---|
GZonePlayer |
name, description, experience, currency, imageId, isDisabled, createdOnDateTime
|
GZoneGame |
name, description, experience, currency, maxOccurrences, imageId, rewards, attributes
|
GZoneRound |
game, leaderboard, startOnDateTime, endOnDateTime, completedOnDateTime, isObjectiveAchieved, isCompleted
|
GZoneLeaderboard |
name, description, attributes, startOnDateTime, endOnDateTime, currentRoundStartOnDateTime, currentRoundEndDateTime, DEFAULT
|
GZoneLeaderboardAttributes |
name, description, image, the display strings configured in the portal |
GZoneLeaderboardPlayerRank |
rank, rankPercentage, value, groupRank, player, rivals
|
GZoneLeaderboardPlayerUnqualifiedRank |
qualified, unqualifiedRank, unqualifiedRankPercentage, value, player, rivals
|
GZonePlayerGroup |
name, description, isPrivate, DEFAULT
|
GZoneExperienceLevel |
name, description, imageId, experienceFrom, experienceTo
|
GZoneReward |
name, description, imageId
|
GZoneNotification |
round, createdOnDateTime
|
Badge |
badgeName, badgeImageUrl, currencyAmountAwarded, experiencePointAmountAwarded, descriptionShort, descriptionFull, status
|
Challenge |
id, ruleName, startOn, endOn, status, imgUrl, currencyAmount, experiencePointAmount
|
Experience |
balance, experienceLevels
|
ExperienceLevel |
experienceLevelName, experiencePointStart, experiencePointEnd, four image locations |
All date fields arrive as strings and are exposed a second time as Joda DateTime through a ...DateTime property.
Read the typed property and leave the raw string alone.
- Every rank call needs a round, selected by
startOn. Read the leaderboard first, then pass itscurrentRoundStartOnthrough. -
rankFromandrankToare inclusive and one based. Ask for the window you are about to draw, not the whole table. - A player who has never driven has no rank at all, and the call succeeds with a null result. Treat that as "not on the board yet", not as an error.
- Only
getGZoneRoundstakes the caching flags; passinvalidateCache = truethere when the result must reflect the latest server state. The rest always go to the network, so hold them in a ViewModel rather than refetching on every configuration change.
Consolidated SDK
SmartDrive
PortalApiBase
PortalApiClient
Wellness
SBeacon
- Overview
- Installation
- Initialization
- Beacon Scanning
- Beacon Pairing
- Trip Management
- Driving Behavior
- Calibration
- Event Handling
- Data Models
- API Reference
Consolidated SPM
SDApiManager
CCFrameworkOpen
SmartDrive
SBeacon