Docs → Android

Android documentation

ANDROID

Stream logs, crashes and user feedback from your Android app to your LogWatcher server. Written in Kotlin, callable from Kotlin and Java. Android 4.1+ (minSdk 16). Persistent SQLite queue, crash handler, logcat mirroring.

Getting started

You need an application key. Create an account, add an app in the dashboard, and copy the key shown on its card. Every SDK call is tied to that key.

Install the SDK

The SDK is a standalone Gradle library module (gr.fintechiq.logwatcher). Build the AAR once and drop it into your app — Maven publication is planned.

# build the AAR from the LogWatcher repository
cd source/client-sdks/android/logwatcher-sdk
./gradlew assembleRelease
# → build/outputs/aar/logwatcher-sdk-release.aar

Copy it to app/libs/ and reference it:

// app/build.gradle
dependencies {
    implementation files('libs/logwatcher-sdk-release.aar')
}

The SDK needs the INTERNET permission (already declared in its manifest).

Initialize

Call init() once, in Application.onCreate(). URL overrides must come before init().

// Kotlin — in Application.onCreate()
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        LogWatcher.setApiUrl("https://logwatcher.app/")
        LogWatcher.init(this, "YOUR_APP_KEY", true)  // true = enable crash reporting
    }
}
// Java — the facade is @JvmStatic, so calls are identical
LogWatcher.setApiUrl("https://logwatcher.app/");
LogWatcher.init(this, "YOUR_APP_KEY", true);

The optional fourth argument enableAndroidLogging (default true) mirrors every LogWatcher call to logcat while you develop.

Send logs

One method per level, same shape as android.util.Log. Logs are queued on-device and uploaded in batches; if the network is down they are retried later.

LogWatcher.d("PaymentFlow", "card inserted · reading chip")
LogWatcher.i("PaymentFlow", "auth approved · code 00")
LogWatcher.w("Network", "settle latency 2.1s — retrying")
LogWatcher.e("PaymentFlow", "auth declined · code 05")
LogWatcher.t("Lifecycle", "onResume")   // trace
LogWatcher.f("Watchdog", "unrecoverable state")  // fatal

Each of these records where it was called — the method, its source file and the line, read from the call stack — and the dashboard shows them when you click a log line. A minified build is named as it was built: keep SourceFile and LineNumberTable (-keepattributes) for the file and the line.

An app that logs through a class of its own — a logging facade, an appender — would be named by that class on every line. Pass the real place yourself with LogWatcher.log(lineNumber, method, file, level, tag, text), which stores what it is given.

LevelMethodWire value
Debugd()0
Warningw()1
Errore()2
Tracet()3
Infoi()4
Fatalf()5

The non-sequential wire values are part of the LogWatcher wire protocol — the SDK handles the mapping.

Crash reporting

Pass true as the third argument of init() and the SDK installs an uncaught-exception handler. Crashes are reported with the exception summary and full stack trace, then control is handed to the previous handler. You can also report one explicitly:

LogWatcher.sendCrash("Checkout crashed", stackTraceString)

Issues & user feedback

Report a noteworthy condition as an issue, or collect a message from the user. Each call returns a URL deep-linking to the item in your dashboard.

val url = LogWatcher.sendIssue("Sync failed", "3 retries exhausted on /settle")
LogWatcher.sendUserFeedback("From the merchant", "Receipt printed twice today")

For a ready-made feedback screen, launch the bundled activity:

val intent = LogWatcher.getUserFeedbackActivityIntent(
    context, "Send feedback", "Tell us what happened",
    "Subject", "Send", "Close")
startActivity(intent)

Device key-values

Attach searchable properties to a device — a store number, a merchant id, a build flavour:

LogWatcher.setDeviceString("store", "ATH-0042")
LogWatcher.setDeviceInteger("lane", 2)
LogWatcher.setDeviceBoolean("pilot", true)
LogWatcher.removeDeviceKey("pilot")

Remote device control

On every launch the SDK registers the device and receives its remote flags. From the dashboard you can enable log collection for a single device (a misbehaving terminal) while the rest of the fleet stays quiet — no release, no restart loop. To force collection locally regardless of the remote flag:

LogWatcher.setForceEnabled(true)   // override the remote toggle
LogWatcher.forceSendOnce()          // flush the queue right now

Encrypted applications

For an application that was created as encrypted, give the SDK the application's shared key — the 16 characters the dashboard showed once, when the application was created. The SDK then ciphers log messages, issue, crash and feedback texts and key-value values on the device; the server stores ciphertext only. Call it before init(), like the URL setters:

LogWatcher.setEncryptionKey("K7Q2-M9XH-4TBD-W3NP")   // case, spaces and dashes do not matter
LogWatcher.init(this, "YOUR_APP_KEY", true)
SituationWhat the SDK does
The key is this application'sSeals everything it uploads. Nothing readable leaves the device.
Wrong key, or the application is not encryptedUploads nothing — never readable content as a fallback. Logs stay queued as in offline mode, one ENCRYPTION: error under the LogWatcher tag in logcat says why, and the server is asked again every 5 minutes.
No key set, the application is encryptedThe server switches this install's logging off; the dashboard marks the device SDK has no key.
setEncryptionKey() after init(), or with an empty valueNothing more is uploaded, and the error says what to fix.

Do not call setEncryptionKey() for an application that is not encrypted. The key is the same for every install, and a constant compiled into an app can be extracted from it: an app that must not carry the key fetches it from its own backend or device management and passes that string. The key is stretched once per app start on a background thread — a fraction of a second on a current phone, more on an old terminal — and minSdk stays 16.

Check an application key

An app that lets a person enter the application key — on a setup screen, or from device management — can ask the server about the key before it uses it. A mistyped key otherwise shows only as uploads the server refuses. The check stands apart from logging: it works before init(), changes nothing the SDK runs with, and registers no device.

LogWatcher.checkApplication("https://logwatcher.app/", appKey) { check ->   // on the main thread
    when (check.status) {
        ApplicationCheck.Status.KNOWN -> { /* check.name, check.isEncrypted */ }
        ApplicationCheck.Status.UNKNOWN_KEY -> { /* the server does not know this key */ }
        ApplicationCheck.Status.NOT_CHECKED -> { /* no answer: check.reason */ }
    }
}
StatusMeaning
KNOWNThe key is an application's: name is what the dashboard calls it, isEncrypted whether it needs a shared key.
UNKNOWN_KEYThe server answered that it does not know the key. Nothing will be delivered with it.
NOT_CHECKEDNo answer — no connection, a time-out, a server that does not offer the check. reason says which; it says nothing about the key.

For an encrypted application, pass the shared key as a third argument: check.encryptionKeyMatches then says whether it is the application's key. The test is made on the device — the shared key is never sent. The address must be https: an application key is not sent over plain http.

See what is delivered

The SDK queues what the app logs and uploads it on its own schedule. A delivery listener tells the app what happens to it — to show on a diagnostics screen, or to notice that nothing arrives:

LogWatcher.setDeliveryListener { status ->        // on the main thread; null removes it
    status.state             // IDLE, SENDING or FAILED
    status.waiting           // log lines and issues still on the device
    status.delivered         // accepted by the server since the app started
    status.lastDeliveredAt   // when the server last accepted something
    status.failure           // when FAILED: its kind, and a message
}
Failure kindMeaning
UNKNOWN_APP_KEYThe server does not know the application key (HTTP 401).
NO_CONNECTIONNo answer from the server: no network, a time-out.
SERVERThe server answered with another error.
ENCRYPTION_KEYContent is held for the shared key of an encrypted application.

The listener is called at once with the current status and then at every change; a burst of log lines makes a few calls, not one per line. status.isOnline and status.isLoggingEnabled say whether uploads are paused (setSendLogsOnline(false)) and whether the server has switched logging off for the device. The status carries counts, times and reasons, never log content; without a listener the SDK behaves exactly as before.

Advanced options

CallWhat it does
setApiUrl(url)Point the SDK at your server. Before init().
setBaseUrl(url)Base URL used to build dashboard deep-links. Before init().
overrideDeviceName(name)Report a custom device name. Before init().
setEncryptionKey(key)The shared key of an encrypted application. Before init().
setMaximumLocalStorageSize(bytes)Cap the on-device log queue (default 5 MiB); oldest entries are dropped first.
getDeviceUrl() / getSessionUrl()Dashboard deep-links for this device / session.
enableUIEventLogging(app)Records Activity lifecycle transitions, rendered as TRANSITION rows in the log viewer.
getUserFeedbackActivityIntent(...)Intent for the bundled user-feedback screen.
enableLogcatLogging()Present for API parity; full behaviour ships in a later SDK release.
Ready to see your first remote log?
Create an app, copy its key, and init the SDK.
Get started