Skip to content

Android PortalApiClient Gamification

AigarsM edited this page Sep 28, 2026 · 1 revision

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.

1. Two generations of the API

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.

2. Concepts

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.

3. Building a gamification screen

Identifiers

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.

Data to load

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()
    )
}

Deriving game state

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
}

When to refresh

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

4. The player

// Defaults to the signed in user
val player = PortalApi.getGZonePlayer().result

player?.name
player?.experience
player?.currency
player?.imageId

5. Games and rounds

val 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
).result

getGZoneLeaderboardRounds 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.

6. Leaderboards and ranks

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
).result

startOn 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).result

Group 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)
).result

getGZonePlayerLeaderboards(playerName) lists the leaderboards a player appears on, which is the right starting point when a tenant runs more than the default table.

7. Groups

val allPublic = PortalApi.getGZoneAllPublicPlayerGroups().result

PortalApi.addGZonePlayerToPublicGroup(groupName = "CityDrivers", playerName = playerName)
PortalApi.removeGZonePlayerFromPublicGroup(groupName = "CityDrivers", playerName = playerName)

val mine = PortalApi.getGZonePlayerGroups(isPrivate = false).result

Joining and leaving is only defined for public groups. Private group membership is managed in the portal.

8. Notifications and images

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?.game

Images 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.

9. Classic gamification

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.

10. Data model reference

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.

11. Practical notes

  • Every rank call needs a round, selected by startOn. Read the leaderboard first, then pass its currentRoundStartOn through.
  • rankFrom and rankTo are 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 getGZoneRounds takes the caching flags; pass invalidateCache = true there 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.

Clone this wiki locally