Android PortalApiClient framework - scopetech/Mobile-Public GitHub Wiki
PortalApiClient SDK Documentation - v1.2.0 (28 Sep, 2026)
This document describes how ScopeTechnology framework "PortalApiClient" works and how to integrate into your application. While the target audience is coders/developers, it brings better understanding for those who are not too tech-savvy as well.
PortalApiClient framework main functionality is to provide an interface to the Connected Cars Portal backend, wrapping up all necessary APIs. It sits on top of PortalApiBase, which supplies the HTTP stack, the session and the trip APIs. PortalApiClient adds everything the portal offers around a trip: the user and vehicle profile, analytics, notifications, beacons, consents, places, fuel, social connections and gamification.
1. Installation
The framework is published to GitHub Packages as an Android library (.aar).
repositories {
maven {
url = uri("https://maven.pkg.github.com/scopetech/Frameworks-Mobile")
credentials {
username = github_username
password = github_key
}
}
}
dependencies {
implementation 'com.scopetechnology:portal-api-client:1.2.0'
}
portal-api-base is exposed as an api dependency, so BasePortalApi and the shared model classes are on your compile
classpath automatically. You do not need to declare it yourself.
Requirements:
| Item | Value |
|---|---|
| minSdkVersion | 24 |
| compileSdkVersion | 36 |
| Java / Kotlin JVM target | 17 |
| Namespace | com.scope.portalapiclient |
2. Initialization
Initialize the framework once, from Application.onCreate(), before any other call.
val config = PortalApiConfig(
baseUrl = "https://your-portal-host/",
appId = "YourAppName",
versionName = BuildConfig.VERSION_NAME
)
PortalApi.init(
applicationContext = this,
config = config,
vehiclePermissions = listOf(1, 7)
)
vehiclePermissions is the list of portal permission ids the app expects a vehicle to carry. Vehicles that do not carry
one of them are filtered out of the user's vehicle list.
PortalApi.init also starts CCManager, the internal bridge that answers beacon firmware and battery events raised by
the SBeacon framework. Nothing else is needed to make firmware upgrade checks work.
To point a running app at a different tenant or host, call PortalApi.reconfigure(config). It swaps the base URL without
losing the current session.
An optional BasePortalApi.Listener can be passed to init to be notified when the session is dropped by the backend,
and an optional QuantiGoApiConfig enables the marketplace calls described in section 7.
3. Requests and responses
Every call on PortalApi is a suspend function that returns ServiceResponse<T?>. Nothing throws on a failed request.
lifecycleScope.launch {
val response = PortalApi.getUser()
if (response.isSuccessful) {
val user = response.result
} else {
val error = response.errorCode
val message = response.errorMessage
}
}
ServiceResponse carries:
| Member | Meaning |
|---|---|
isSuccessful |
true when no error was recorded |
result |
the parsed body, nullable |
errorCode |
an IServiceError, normally a ServiceError constant |
errorMessage |
a message, sometimes technical, not always present |
isResponseFromCache |
the body came from the offline cache, not the network |
httpCode |
the HTTP status, or null when no HTTP response was received |
ServiceError is an enum of portal failure cases (AUTHENTICATION_ERROR, NO_VEHICLES, PASSWORD_WEAK,
CONSENT_NOT_AVAILABLE and so on). CommonServiceErrorExtractor maps the numeric error codes the portal returns onto
those constants, and each constant resolves to a localized string resource shipped with the framework. English, Spanish
and Arabic are included. Show ServiceError.getErrorMessage(error) rather than errorMessage when the text reaches a user.
4. Session
// Token only, no profile
val token = PortalApi.retrieveUserToken(username, password)
// Token plus the full InsuredPerson profile, this is the normal login
val login = PortalApi.standardAuth(username, password)
PortalApi.isLoggedIn()
PortalApi.logout(applicationContext)
After a successful login the profile is cached in the framework and exposed through plain getters and through LiveData:
PortalApi.getUserId()
PortalApi.getUsername()
PortalApi.getFirstName()
PortalApi.getSelectedVehicleId()
PortalApi.getInsuredVehicles()
PortalApi.getUserSmartDriveSubscriptionId()
PortalApi.getLiveInsuredPerson().observe(this) { person -> }
PortalApi.getLiveSelectedVehicle().observe(this) { vehicle -> }
getUser() refreshes the profile and updates the cached copy. getSelf() returns the same payload without touching the
cached copy, which is what you want for a background refresh that must not disturb the UI.
Password recovery uses resetPassword(username) to send the code and setNewPassword(body) to apply it. One time
password registration and login are available through requestOtp, registerWithOtp and authWithOtp.
5. Response caching
Most read calls accept three flags that map onto the caching headers the portal understands.
| Parameter | Effect |
|---|---|
saveResponseInCache |
store this response so it can be served offline later |
useOfflineCache |
serve the cached copy when the network call fails |
invalidateCache |
drop the cached copy and go to the network |
They default to a sensible combination, usually cache on, offline on, and invalidate only when the network is reachable.
Check response.isResponseFromCache before showing a timestamp to the user.
6. What the framework covers
The Kotlin PortalApi object is the current surface. Calls are grouped by region in the source file.
| Group | Representative calls |
|---|---|
| User and vehicles | getUser, getSelf, getVehicleDetails, getVehicleLastKnownPosition, terminatePolicy |
| Trips | getTripsAsJson, sendBatchWithTripData, assignTrip, clearDriver, getTripScore |
| MZone trips and events | getMZoneTrips, getMZoneTripEvents, getMZoneEventTypes, getMZoneLabels, setMZoneLabels, getMZoneVehicleDtcs |
| Driving analytics | getAnalyticsScore, getAnalyticsExpressions, getAnalyticsVariables, getAnalyticsTripExpressions, getDriverAvailability, the ...OverPeriod variants |
| Driving summary and feedback | getDrivingSummary, getFeedback, getTripAnalyticsDetails, sendUserFeedback |
| Notifications | getActiveNotifications, updateNotificationsCheckTime, registerPushNotificationToken |
| Beacons | getBeacons, getAllInstalledBeacons, getRecordingBeacon, setRecordingBeacon, getBeaconBatteryStatus, updateBeaconBatteryStatus |
| Accessory beacons | getAvailableAccessoryBeacons, linkAccessoryBeacon, unlinkAccessoryBeacon, getAccessoryBeacon |
| Firmware | firmwareUpdateAvailable, downloadFirmwareFile, updateBeaconFirmware |
| Consents | getAllConsents, getConsentedConsents, setConsent, revokeConsent |
| GDPR | requestGDPRChangeData, requestGDPRDataCopy, requestGDPRDeleteData |
| Social connections | createSocialConnection, getReceivedSocialConnections, getActiveSocialConnections, acceptSocialConnection, rejectSocialConnection, deactivateSocialConnection |
| Avatars | setUserAvatarKey, getUserAvatarKey, deleteUserAvatarKey |
| Configuration | getConfig, getSubscriptionInfo, getMProfilerConfig, getPortalSettingsTripDistanceThreshold |
| Gamification | see Gamification |
| Marketplace | see section 7 |
6.1. Consents
Consent definitions are fetched, then set or revoked by name. There is one body class, ConsentSetBody, and its
policyId is optional: leave it unset to act on the signed in user, pass it to act on one specific policy. Gson omits
null fields, so an unset policy is absent from the request rather than sent as null.
val definitions = PortalApi.getAllConsents()
val accepted = PortalApi.getConsentedConsents()
// The signed in user. These two overloads are shorthand for ConsentSetBody(name).
PortalApi.setConsent("MarketingEmails")
PortalApi.revokeConsent("MarketingEmails")
// A specific policy
PortalApi.setConsent(ConsentSetBody(name = "MarketingEmails", policyId = policyId))
PortalApi.revokeConsent(ConsentSetBody(name = "MarketingEmails", policyId = policyId))
6.2. Raw passthrough
When the portal exposes something the framework does not wrap yet, getSomething and postSomething issue an
authorized request against an absolute URL and hand back the raw body as a String.
val raw = PortalApi.getSomething(
url = "https://your-portal-host/v1/api/SomeNewEndpoint",
params = mapOf("pvu" to vehicleId)
)
Treat this as a stopgap. Anything used more than once belongs in the framework.
7. QuantiGo marketplace
The marketplace calls are inert until a QuantiGoApiConfig is supplied, either through PortalApi.init or later through
PortalApi.initQuantiGo(config). The configuration itself is served by the portal through getQuantigoConfig(), so the
usual sequence is to read the config, then initialize, then call the marketplace.
PortalApi.getQuantigoConfig().result?.let { config ->
PortalApi.initQuantiGo(QuantiGoApiConfig(config))
}
val vouchers = PortalApi.getQuantigoUserVouchers()
val marketplaces = PortalApi.getQuantigoMarketplaces()
val balance = PortalApi.getQuantigoMarketplaceBalance(marketplaceName)
8. Legacy Java client
PortalApiClient is the original callback based facade. It is still shipped because several feature areas have not been
ported to the Kotlin object yet: service jobs and workshop quotes, points of interest and place rules, fuel entries,
badges and challenges, and avatar image upload.
PortalApiClient.getInstance().getUser(new ServiceCallback<ServiceResponse<InsuredPerson>>() {
@Override
public void onResult(ServiceResponse<InsuredPerson> response) {
if (response.isSuccessful()) {
InsuredPerson person = response.getResult();
}
}
});
Callbacks are delivered on the main thread. Both facades share one session, so you can mix them freely. For anything that
exists in both, prefer PortalApi, and treat PortalApiClient as frozen.
9. Driving behaviour events filtering
Threshold filtering is inherited from PortalApiBase. Trips fetched through BasePortalApi.getTrips already have
sub threshold acceleration, braking and cornering events removed; MZone events are filtered by the app against the
tenant thresholds. The mechanism and a worked example are documented in
PortalApiBase, section 1.
10. ProGuard and R8
The library ships its own consumer rules for Retrofit and OkHttp. Model classes are deserialized by Gson through
@SerializedName, so field names survive obfuscation and no extra keep rule is required for them. If you serialize the
model classes yourself by reflection, add your own keep rules.
11. Notes on the open source distribution
The public distribution of this framework is built from a dedicated branch with all tenant specific code removed and is
published as CCFramework-Android-Open. That repository carries
its own README.md and Gamification_QA.md, laid out the way the iOS package documents itself. If you are integrating
against it, be aware that:
- Endpoints written for one named customer are not part of this build. The generic equivalents are.
- The instrumentation test suite talks to a live portal, so the host and the account are passed in as instrumentation arguments rather than being checked in. Without them the suite skips.
- Some model classes remain in the build without a call that returns them. They are kept so that existing integrations keep compiling.