commit e3f80c058f375ba5f5d34a4e72729d345742bc66 Author: Niko Marmeladkov Date: Mon Oct 5 16:18:00 2026 +0300 NikoGPT Android 1.0.0: open-source WebView client for chat.n1ko.dev - Android 10+ (minSdk 29), target/compile SDK 35 - chat.n1ko.dev only; other links open in the system browser - system file picker for attachments, DownloadManager for files - TLS never bypassed, no cleartext, no analytics - pull-to-refresh, back navigation, progress and retry screen diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..36d73ba --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,22 @@ +name: build +on: + push: + tags: ["v*"] + workflow_dispatch: +jobs: + apk: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + - uses: android-actions/setup-android@v3 + - name: Build debug APK + run: ./gradlew --no-daemon assembleDebug + - name: Upload APK + uses: actions/upload-artifact@v4 + with: + name: nikogpt-android-debug + path: app/build/outputs/apk/debug/app-debug.apk diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9d2fc3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.gradle/ +build/ +local.properties +*.iml +.idea/ +.DS_Store +captures/ +.externalNativeBuild/ +.cxx/ +*.keystore +*.jks diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..420357b --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Niko Marmeladkov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..265c614 --- /dev/null +++ b/README.md @@ -0,0 +1,60 @@ +# NikoGPT for Android + +Open-source Android client for the NikoGPT web chat +([chat.n1ko.dev](https://chat.n1ko.dev)) — a thin, auditable WebView shell +around the same web interface that runs in the browser. No analytics, no +trackers, no third-party SDKs: the whole app is one activity and a handful of +resources. + +- **Android 10+** (`minSdk 29`), targets Android 15. +- **Everything the web app can do**: conversations, attachments (system file + picker), images, generated files, settings, themes. +- Downloads go through Android's `DownloadManager` (with your session + cookies), links to other sites open in the system browser. +- **Open in the browser only**: `chat.n1ko.dev` loads inside; anything else + leaves the app. +- TLS errors are never bypassed, no cleartext traffic, no backup. +- Day/night theme follows the system; the page keeps its own theme setting. +- Back gesture navigates the chat history, pull-to-refresh reloads. + +## Install + +Download the APK from the [releases page](../../releases) and open it on the +phone (allow installing apps from this source when Android asks). The APK is +debug-signed for now, so Android may show a warning. + +## Build + +Requirements: JDK 17, Android SDK (platform 35, build-tools 35.0.0). + +```bash +./gradlew assembleDebug # app/build/outputs/apk/debug/app-debug.apk +./gradlew assembleRelease # unsigned; sign it yourself +``` + +Or just open the project in Android Studio. + +### Release signing + +Create a keystore and export it as Gradle properties or use +`apksigner` on the release output: + +```bash +keytool -genkeypair -v -keystore nikogpt.jks -alias nikogpt \ + -keyalg RSA -keysize 2048 -validity 10000 +apksigner sign --ks nikogpt.jks --out nikogpt-release.apk \ + app/build/outputs/apk/release/app-release-unsigned.apk +``` + +## Structure + +| Path | Purpose | +|---|---| +| `app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt` | the whole app: WebView setup, file chooser, downloads, back handling | +| `app/src/main/res/layout/activity_main.xml` | WebView + progress bar + retry screen | +| `.forgejo/workflows/build.yml` | CI: builds the APK on tags | + +## License + +MIT — see [LICENSE](LICENSE). The Tabler-based icons used by the web chat come +from [f.n1ko.dev/assets/ui](https://f.n1ko.dev/assets/ui) (MIT). diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..1e1fc4f --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,49 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") +} + +android { + namespace = "dev.n1ko.nikogpt" + compileSdk = 35 + + defaultConfig { + applicationId = "dev.n1ko.nikogpt" + minSdk = 29 // Android 10 + targetSdk = 35 + versionCode = 1 + versionName = "1.0.0" + } + + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = "17" + } + + buildFeatures { + buildConfig = true + } +} + +dependencies { + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.activity:activity-ktx:1.9.3") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation("androidx.webkit:webkit:1.12.1") +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..a2778b5 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,4 @@ +# Keep the WebView bridge classes if @JavascriptInterface is ever added. +-keepclassmembers class * { + @android.webkit.JavascriptInterface ; +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b76d809 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt b/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt new file mode 100644 index 0000000..44a4b49 --- /dev/null +++ b/app/src/main/java/dev/n1ko/nikogpt/MainActivity.kt @@ -0,0 +1,219 @@ +package dev.n1ko.nikogpt + +import android.annotation.SuppressLint +import android.app.DownloadManager +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.net.http.SslError +import android.os.Bundle +import android.os.Environment +import android.view.View +import android.webkit.CookieManager +import android.webkit.DownloadListener +import android.webkit.SslErrorHandler +import android.webkit.URLUtil +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updatePadding +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout + +/** + * The whole app: a thin, open-source WebView around the NikoGPT web chat. + * + * Only chat.n1ko.dev is loaded inside; every other link opens in the system + * browser. File attachments go through the system document picker, downloads + * through DownloadManager. TLS errors are never bypassed. + */ +class MainActivity : AppCompatActivity() { + + companion object { + private const val START_URL = "https://chat.n1ko.dev/" + private const val HOST = "chat.n1ko.dev" + private const val UA_SUFFIX = " NikoGPT-Android/1.0" + } + + private lateinit var webView: WebView + private lateinit var refresh: SwipeRefreshLayout + private lateinit var progress: ProgressBar + private lateinit var errorBox: LinearLayout + private lateinit var errorText: TextView + + private var fileCallback: ValueCallback>? = null + private val filePicker: ActivityResultLauncher> = + registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> + fileCallback?.onReceiveValue(uris?.toTypedArray() ?: arrayOf()) + fileCallback = null + } + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) + setContentView(R.layout.activity_main) + + webView = findViewById(R.id.webview) + refresh = findViewById(R.id.refresh) + progress = findViewById(R.id.progress) + errorBox = findViewById(R.id.error_box) + errorText = findViewById(R.id.error_text) + findViewById