Android documentation
ANDROIDStream 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.
| Level | Method | Wire value |
|---|---|---|
| Debug | d() | 0 |
| Warning | w() | 1 |
| Error | e() | 2 |
| Trace | t() | 3 |
| Info | i() | 4 |
| Fatal | f() | 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)
| Situation | What the SDK does |
|---|---|
| The key is this application's | Seals everything it uploads. Nothing readable leaves the device. |
| Wrong key, or the application is not encrypted | Uploads 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 encrypted | The server switches this install's logging off; the dashboard marks the device SDK has no key. |
setEncryptionKey() after init(), or with an empty value | Nothing 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 */ }
}
}
| Status | Meaning |
|---|---|
KNOWN | The key is an application's: name is what the dashboard calls it, isEncrypted whether it needs a shared key. |
UNKNOWN_KEY | The server answered that it does not know the key. Nothing will be delivered with it. |
NOT_CHECKED | No 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 kind | Meaning |
|---|---|
UNKNOWN_APP_KEY | The server does not know the application key (HTTP 401). |
NO_CONNECTION | No answer from the server: no network, a time-out. |
SERVER | The server answered with another error. |
ENCRYPTION_KEY | Content 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
| Call | What 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. |