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.