package dev.n1ko.nikogpt import android.annotation.SuppressLint import android.app.AlertDialog 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.JavascriptInterface 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.content.FileProvider import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import androidx.credentials.CredentialManager import androidx.credentials.GetCredentialRequest import androidx.credentials.exceptions.GetCredentialException import androidx.lifecycle.lifecycleScope import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInOptions import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability import com.google.android.gms.common.api.ApiException import com.google.android.libraries.identity.googleid.GetGoogleIdOption import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential import java.io.File import kotlinx.coroutines.launch /** * 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" // The web app reads this marker to switch to the app handoff flow. private val UA_SUFFIX = " NikoGPT-Android/" + BuildConfig.VERSION_NAME } 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 // Shared text ("Share to NikoGPT") waiting for the page to load. private var pendingShare: String? = null // Where the camera writes the picture before it reaches the web app. private var pendingCameraUri: Uri? = null private var fileCallback: ValueCallback>? = null private val filePicker: ActivityResultLauncher> = registerForActivityResult(ActivityResultContracts.OpenMultipleDocuments()) { uris -> fileCallback?.onReceiveValue(uris?.toTypedArray() ?: arrayOf()) fileCallback = null } // microG path: the legacy Google Sign-In API, which microG implements. private var legacyLinkMode = false private val legacySignIn: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val link = legacyLinkMode try { val account = GoogleSignIn.getSignedInAccountFromIntent(result.data) .getResult(ApiException::class.java) val token = account?.idToken if (!token.isNullOrBlank()) deliverToken(token, link) else openGoogleInBrowser(link) } catch (e: ApiException) { openGoogleInBrowser(link) } catch (e: Exception) { openGoogleInBrowser(link) } } // Camera capture for the attach button. private val takePicture: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.TakePicture()) { ok -> val cb = fileCallback fileCallback = null val uri = pendingCameraUri pendingCameraUri = null if (ok && uri != null) cb?.onReceiveValue(arrayOf(uri)) else cb?.onReceiveValue(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