Newer
Older
navi-1 / docs / android-client.md
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 9 May 5 KB Document Android OAuth bridge page flow

Android Client

Native Android app that wraps the Navi web client in a WebView.

Location

android-client/ — standalone Android project (Kotlin, Gradle).

Architecture

Single-activity app with two screens:

Activity Purpose
SetupActivity First-launch screen — prompts for server URL, saves to SharedPreferences
MainActivity Main screen — full-screen WebView loading the Navi web client

Server URL is stored in SharedPreferences under key server_url. If absent on launch, SetupActivity is shown. If the WebView fails to load the main frame, the saved URL is cleared so the next launch prompts again.

Build & Deploy

cd android-client

# Debug APK
./gradlew assembleDebug

# Install via adb (device connected via USB)
adb install -r app/build/outputs/apk/debug/app-debug.apk

# One-liner: build + install
./gradlew assembleDebug && adb install -r app/build/outputs/apk/debug/app-debug.apk

WebView configuration

Setting Value Reason
javaScriptEnabled true Required for the Vue app
domStorageEnabled true Session state, composables
cacheMode LOAD_NO_CACHE Always fetch fresh from server
mixedContentMode ALWAYS_ALLOW Local server may serve mixed content
userAgentString …original… NaviAndroid/1.0 Platform detection in web client

shouldOverrideUrlLoading intercepts navigation:

  • Normal Navi app URLs on the configured server host → handled by WebView.
  • API, iframe, and artifact preview loads without a user gesture → handled by WebView.
  • External hosts → opened via Intent.ACTION_VIEW in the system browser.
  • User-clicked same-host artifact/file URLs → opened via Intent.ACTION_VIEW in the system browser:
    • /content-viewers/...
    • /sessions/{session_id}/files/{filename}
    • URLs with download=1

This means inline preview cards still render inside the app, but when the user explicitly opens a preview, raw file, download link, or external URL, it leaves the app and opens in the user's browser.

Platform detection in web client

The app appends NaviAndroid/1.0 to the WebView's User-Agent. The web client can detect this:

import { isAndroid } from '@/composables/usePlatform.js'

if (isAndroid) {
  // Android-specific behaviour
}

usePlatform.js is a single-line composable:

export const isAndroid = navigator.userAgent.includes('NaviAndroid')

Use this for any future UI differences between the web client and the Android app (hiding elements, adjusting layout, etc.).

OAuth / login flow

Android WebView does not share cookies with the system browser. When the user clicks Login, the app opens gnexus-auth in the user's default browser (Chrome, DuckDuckGo, etc.) so SSO works. After successful authentication, the backend redirects the browser to /auth/mobile-done?sid=<session_id> instead of setting a cookie.

/auth/mobile-done is a bridge page styled with the gnexus UI kit. It:

  1. Immediately attempts window.location.href = "intent://auth/callback?sid=xxx#Intent;scheme=navi;package=com.navi.client;end" — Chrome understands this and opens the app automatically.
  2. If the browser blocks the redirect (DuckDuckGo, Firefox), a styled fallback button "Open Navi App" appears after 1.5s. Tapping it triggers the same Intent URL with a user gesture, which the browser cannot block.

AndroidManifest.xml declares an intent filter for navi://auth/callback:

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="navi" android:host="auth" android:pathPrefix="/callback" />
</intent-filter>

The activity uses launchMode="singleTask" so onNewIntent is called even when the app is already running.

MainActivity handles the incoming intent in both onCreate (cold start) and onNewIntent (app already running):

override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    intent.data?.let { uri ->
        if (uri.scheme == "navi" && uri.host == "auth" && uri.path == "/callback") {
            val sid = uri.getQueryParameter("sid")
            val prefs = getSharedPreferences("navi", Context.MODE_PRIVATE)
            val serverUrl = prefs.getString("server_url", null)
            if (sid != null && serverUrl != null) {
                CookieManager.getInstance().setCookie(
                    serverUrl,
                    "navi_auth_session=$sid; Path=/"
                )
                webView.loadUrl(serverUrl)
            }
        }
    }
}

After the cookie is set, the WebView reloads the server URL and the user is authenticated.


Back navigation

Hardware back button navigates WebView history if available (webView.canGoBack()), otherwise falls through to the default Android behaviour (closes the activity).

File / image picker

WebChromeClient.onShowFileChooser is implemented to support image attachment in chat:

  • Requests CAMERA and READ_MEDIA_IMAGES / READ_EXTERNAL_STORAGE permissions at runtime
  • Presents a chooser combining gallery picker and camera capture
  • Camera photos are saved to getExternalFilesDir(DIRECTORY_PICTURES) via FileProvider