> ## Documentation Index
> Fetch the complete documentation index at: https://developer.plugchoice.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Android

> Add the Plugchoice SDK to an Android app, open Link and handle the result.

export const Screen = ({name, alt, caption, wide = false}) => {
  const style = wide ? {
    width: '100%'
  } : {
    width: '280px',
    maxWidth: '100%'
  };
  return <figure style={{
    margin: '1.5rem 0',
    textAlign: 'center'
  }}>
      <img className="block dark:hidden" src={`/images/sdk/${name}-light.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      <img className="hidden dark:block" src={`/images/sdk/${name}-dark.webp`} alt={alt} style={{
    ...style,
    margin: '0 auto',
    borderRadius: '12px',
    border: '1px solid rgba(128, 128, 128, 0.25)'
  }} />
      {caption ? <figcaption style={{
    marginTop: '0.5rem',
    fontSize: '0.875rem',
    opacity: 0.7
  }}>{caption}</figcaption> : null}
    </figure>;
};

The Android SDK is the library `com.plugchoice:plugchoice`, for Kotlin apps. It shows Link, the hosted flow at `connect.plugchoice.com`, in a full-screen activity, and does what a web page can't: join a charger's Wi-Fi, find and reach chargers on the local network, talk Bluetooth to them and scan a QR code.

## Install

**Requirements**: Android 10 (API 29) or later (`minSdk 29`), `compileSdk 36`, and JDK 17. Bluetooth on Android 12 and later needs your app to target API 31 or later. Scanning a charger's QR code uses Google Play services; on a device without them, the user types the code instead.

The library is `com.plugchoice:plugchoice` on Maven Central. Make sure `mavenCentral()` is in your repositories (it usually is), then add the dependency to your app module:

<CodeGroup>
  ```kotlin build.gradle.kts theme={null}
  dependencies {
      implementation("com.plugchoice:plugchoice:0.4.0")
  }
  ```

  ```groovy build.gradle theme={null}
  dependencies {
      implementation 'com.plugchoice:plugchoice:0.4.0'
  }
  ```
</CodeGroup>

The public API is in the package `com.plugchoice`.

## Configure your app

### Permissions

The library's manifest adds every permission it uses to your app's manifest. You don't add any. The SDK asks for the runtime ones only when the flow needs them.

| Permission | Why |
| - | - |
| `INTERNET`, `ACCESS_NETWORK_STATE`, `CHANGE_NETWORK_STATE`, `ACCESS_WIFI_STATE`, `CHANGE_WIFI_STATE` | Loading the hosted flow, joining a charger's Wi-Fi and talking to the charger |
| `CHANGE_WIFI_MULTICAST_STATE` | Finding chargers on the home Wi-Fi |
| `NEARBY_WIFI_DEVICES` (Android 13 and later) | Joining a charger's Wi-Fi. Runtime. |
| `ACCESS_FINE_LOCATION`, `ACCESS_COARSE_LOCATION` (up to Android 12L) | Joining a charger's Wi-Fi; on Android 10 and 11 also scanning for Bluetooth chargers. Runtime. |
| `BLUETOOTH_SCAN`, `BLUETOOTH_CONNECT` (Android 12 and later) | Setting chargers up over Bluetooth. Runtime. |
| `BLUETOOTH`, `BLUETOOTH_ADMIN` (up to Android 11) | Setting chargers up over Bluetooth |

Bluetooth LE is declared as an optional feature, so devices without it can still install your app. To leave Bluetooth out, remove the four Bluetooth permissions in your manifest with `tools:node="remove"`; the SDK then doesn't offer it. Bluetooth on Android 12 and later also needs your app to target API 31 or later.

### Plain HTTP to chargers

Some chargers serve their local API over plain HTTP on their own Wi-Fi: Peblar answers at `http://172.16.0.1`. Android blocks plain HTTP by default, and the SDK's requests follow your app's network security configuration, so allow it for that address only:

```xml res/xml/network_security_config.xml theme={null}
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">172.16.0.1</domain>
    </domain-config>
</network-security-config>
```

```xml AndroidManifest.xml theme={null}
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>
```

If your app already has a network security configuration, add the `domain-config` to it. Without this, Peblar chargers can't be set up in your app. The other chargers the SDK sets up today use HTTPS or Bluetooth.

### Google Play data safety

Scanning a charger's QR code uses Google's code scanner from Google Play services. It needs no camera permission, but its ML Kit components send usage data to Google. Mention it in your app's data safety form.

## Create the SDK

Create one `Plugchoice` instance and keep it for your app's lifetime, for example in your `Application` or a singleton. Give it a callback that fetches a client secret from your server:

```kotlin theme={null}
import com.plugchoice.Plugchoice

val plugchoice = Plugchoice(fetchClientSecret = { action ->
    // action.name ("add", "network", …), action.chargerId, action.siteId
    backend.plugchoiceClientSecret(action)
})
```

The callback calls your own endpoint, which creates a client session scoped to the action and returns its secret. [Get started](/sdk/get-started) shows that endpoint. The callback runs on the main thread, so make the call with a suspending client. With OkHttp, it can look like this:

```kotlin theme={null}
class Backend(private val baseUrl: String, private val client: OkHttpClient) {
    suspend fun plugchoiceClientSecret(action: LinkAction): String = withContext(Dispatchers.IO) {
        val body = JSONObject()
            .put("action", action.name)
            .putOpt("chargerId", action.chargerId)
            .putOpt("siteId", action.siteId)
            .toString()
            .toRequestBody("application/json".toMediaType())
        // Add your own authentication: your server needs to know who is signed in.
        val request = Request.Builder().url("$baseUrl/plugchoice/client-secret").post(body).build()
        client.newCall(request).execute().use { response ->
            check(response.isSuccessful) { "client secret: HTTP ${response.code}" }
            JSONObject(response.body!!.string()).getString("clientSecret")
        }
    }
}
```

The SDK calls `fetchClientSecret` when Link opens, while the hosted flow loads. It calls it again, with the same action, whenever the secret expires (after 60 minutes), so a user can take as long as they need. The secret never goes into a URL or an intent, and the SDK never logs it.

## Open Link

Register the SDK's activity result contract, then launch it with an action:

```kotlin theme={null}
class ChargerActivity : ComponentActivity() {
    private val openLink = registerForActivityResult(plugchoice.link.contract()) { result: LinkResult ->
        // Handle the result.
    }

    fun changeNetwork(chargerId: String) = openLink.launch(LinkAction.network(chargerId))
}
```

In Jetpack Compose:

```kotlin theme={null}
val openLink = rememberLauncherForActivityResult(plugchoice.link.contract()) { result -> /* … */ }

Button(onClick = { openLink.launch(LinkAction.addCharger()) }) { Text("Add a charger") }
```

Without the Activity Result API, start `plugchoice.link.intent(context, action)` for a result, and read it in `onActivityResult` with `plugchoice.link.parseResult(resultCode, data)`.

The actions:

| Action | Helper |
| - | - |
| Add a charger, optionally at one of your user's sites | `LinkAction.addCharger()`, `LinkAction.addCharger(siteId)` |
| Change a charger's network | `LinkAction.network(chargerId)` |
| Set a charger up | `LinkAction.setup(chargerId)` |
| Reconnect a charger to Plugchoice | `LinkAction.reconnect(chargerId)` |
| An action this SDK version has no helper for yet | `LinkAction.custom(action, chargerId, siteId)` |

The SDK passes the action to the hosted flow without reading it, so a new action works through `custom` before it gets a helper. Your client session's scope must cover the action's charger or site. [Actions](/sdk/concepts/actions) explains each one.

Link handles rotation, dark mode and keyboard changes itself, so the flow never reloads. It keeps the screen on while it's open, because a charger's Wi-Fi connection drops when the phone locks.

## Handle the result

The contract returns a `LinkResult`:

<ResponseField name="status" type="LinkResult.Status">
  `SUCCESS`, `CANCELLED` or `ERROR`.
</ResponseField>

<ResponseField name="action" type="String">
  The action the link session did. The user may have picked another one from the list of what a charger can do; otherwise it's the action you opened.
</ResponseField>

<ResponseField name="sessionId" type="String?">
  The link session, when the hosted flow started one. `null` when Link closed before that.
</ResponseField>

<ResponseField name="devices" type="List<Device>">
  The devices the link session reported, each `Device(type, id)`. On `SUCCESS`, the devices it finished. `type` is `"charger"` (`Device.TYPE_CHARGER`) today; ignore types you don't know.
</ResponseField>

<ResponseField name="error" type="LinkError?">
  On `ERROR`: a `code`, and a `message` for your logs.
</ResponseField>

```kotlin theme={null}
when (result.status) {
    LinkResult.Status.SUCCESS -> {
        val chargerIds = result.devices.filter { it.type == Device.TYPE_CHARGER }.map { it.id }
        // Tell your server about result.sessionId; it confirms the outcome.
    }
    LinkResult.Status.CANCELLED -> Unit // The user left. Nothing changed.
    LinkResult.Status.ERROR -> Log.w("Plugchoice", "Link failed: ${result.error}")
}
```

The result is for your app's UI. Before you rely on it, your server confirms it with [Get a link session](/sdk/api/get-link-session) (`GET /sdk/v1/link-sessions/{id}`). [Link sessions](/sdk/concepts/link-sessions) explains why.

## Errors

On `ERROR`, `result.error?.code` says why. The SDK sets two codes itself:

| Code | Constant | When |
| - | - | - |
| `pageLoadFailed` | `LinkError.PAGE_LOAD_FAILED` | The hosted flow didn't load (no internet, server unreachable). The SDK showed a "The page did not load" screen with **Try again**, and the user closed it. |
| `internal` | `LinkError.INTERNAL` | The SDK couldn't run the hosted flow: no usable Android System WebView, or its renderer stopped. |

Every other code comes from the hosted flow. The one to know is `clientSecretUnavailable` (`LinkError.CLIENT_SECRET_UNAVAILABLE`): your `fetchClientSecret` threw, returned an empty string or took longer than 30 seconds. The user then sees this screen, with **Try again**, and the result reports `clientSecretUnavailable` if they close it.

<Screen name="secret-unavailable" alt="The hosted flow's error screen when the app couldn't get a client secret, with a Try again button" caption="What the user sees when fetchClientSecret fails" />

The same screen shows when Android recreated Link after your app's process died: the `Plugchoice` instance is gone, so the user starts again from your app.

Other codes are for your logs; don't branch on them. [Errors](/sdk/errors) lists them.

## Show actions where they work

Not every charger can do every action, and not every phone has what an action needs. Show a button only when both are true:

* The charger's [capability](/sdk/concepts/capabilities) for it is `capable`. Your server reads it with [Get a charger](/sdk/api/get-charger).
* Every transport in the capability's `needs` is in `Plugchoice.transports(context)`.

```kotlin theme={null}
// A capability as your server passed it on from the SDK API:
// { "capable": true, "needs": ["ble"] }
data class Capability(val capable: Boolean, val needs: List<String>)

fun canOffer(context: Context, capability: Capability): Boolean =
    capability.capable && Plugchoice.transports(context).containsAll(capability.needs)
```

`Plugchoice.transports(context)` asks for no permission. It returns `wifi` (on a device with Wi-Fi), `http`, `socket` and `lan`, and `ble` when the device has Bluetooth LE and your app kept the SDK's Bluetooth permissions.

## Test on a real device

The emulator can open Link, but it can't reach a charger. Use a phone for:

* joining a charger's Wi-Fi, and the system's "connect to device" prompt
* finding chargers on the home Wi-Fi
* everything Bluetooth, and its permission prompts
* scanning a QR code

The [example app](https://github.com/plugchoice/mobile-sdk/tree/main/android/example) opens Link with a client secret you paste, for any action.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.