package com.navi.client import android.Manifest import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment import android.os.Message import android.provider.MediaStore import android.view.KeyEvent import android.webkit.* import android.webkit.CookieManager import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import java.io.File import java.text.SimpleDateFormat import java.util.* class MainActivity : AppCompatActivity() { private lateinit var webView: WebView private var serverHost: String = "" private var fileChooserCallback: ValueCallback>? = null private var cameraImageUri: Uri? = null private var pendingFileChooserCallback: ValueCallback>? = null private val permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { _ -> pendingFileChooserCallback?.let { cb -> pendingFileChooserCallback = null openFileChooser(cb) } } private val filePickerLauncher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> val uris: Array? = if (result.resultCode == Activity.RESULT_OK) { val data = result.data when { data == null || (data.data == null && data.clipData == null) -> cameraImageUri?.let { arrayOf(it) } data.clipData != null -> { val clip = data.clipData!! Array(clip.itemCount) { clip.getItemAt(it).uri } } else -> data.data?.let { arrayOf(it) } } } else null fileChooserCallback?.onReceiveValue(uris) fileChooserCallback = null } @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val prefs = getSharedPreferences("navi", Context.MODE_PRIVATE) val serverUrl = prefs.getString("server_url", null) // Handle deep link from OAuth bridge page before normal setup. intent?.data?.let { uri -> if (uri.scheme == "navi" && uri.host == "auth" && uri.path == "/callback") { val sid = uri.getQueryParameter("sid") if (sid != null && serverUrl != null) { CookieManager.getInstance().setCookie( serverUrl, "navi_auth_session=$sid; Path=/" ) } } } if (serverUrl.isNullOrBlank()) { startActivity(Intent(this, SetupActivity::class.java)) finish() return } serverHost = Uri.parse(serverUrl).host ?: "" setContentView(R.layout.activity_main) WebView.setWebContentsDebuggingEnabled(true) webView = findViewById(R.id.webview) setupWebView(prefs) webView.loadUrl(serverUrl) } 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) } } } } @SuppressLint("SetJavaScriptEnabled") private fun setupWebView(prefs: android.content.SharedPreferences) { webView.settings.apply { javaScriptEnabled = true javaScriptCanOpenWindowsAutomatically = true domStorageEnabled = true cacheMode = WebSettings.LOAD_NO_CACHE mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW mediaPlaybackRequiresUserGesture = false allowFileAccess = true allowContentAccess = true setSupportMultipleWindows(true) userAgentString = "$userAgentString NaviAndroid/1.0" } webView.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { return if (shouldOpenExternally(request)) openExternal(request.url) else false } override fun onReceivedError(view: WebView, request: WebResourceRequest, error: WebResourceError) { if (request.isForMainFrame) { // Clear saved URL so next launch shows the setup screen prefs.edit().remove("server_url").apply() view.loadData(errorPage(error.description.toString()), "text/html", "utf-8") } } } webView.webChromeClient = object : WebChromeClient() { override fun onCreateWindow( view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message ): Boolean { if (!isUserGesture) return false val popup = WebView(this@MainActivity) popup.webViewClient = object : WebViewClient() { override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean { openExternal(request.url) view.destroy() return true } } val transport = resultMsg.obj as WebView.WebViewTransport transport.webView = popup resultMsg.sendToTarget() return true } override fun onShowFileChooser( view: WebView, callback: ValueCallback>, params: FileChooserParams ): Boolean { fileChooserCallback?.onReceiveValue(null) fileChooserCallback = callback val needed = mutableListOf().apply { if (ContextCompat.checkSelfPermission(this@MainActivity, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) add(Manifest.permission.CAMERA) val storage = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) Manifest.permission.READ_MEDIA_IMAGES else Manifest.permission.READ_EXTERNAL_STORAGE if (ContextCompat.checkSelfPermission(this@MainActivity, storage) != PackageManager.PERMISSION_GRANTED) add(storage) } if (needed.isNotEmpty()) { pendingFileChooserCallback = callback fileChooserCallback = null permissionLauncher.launch(needed.toTypedArray()) } else { openFileChooser(callback) } return true } } } private fun shouldOpenExternally(request: WebResourceRequest): Boolean { val uri = request.url val scheme = uri.scheme?.lowercase(Locale.US) ?: return false if (scheme != "http" && scheme != "https") { return request.hasGesture() } val host = uri.host ?: return false if (host != serverHost) return true if (!request.hasGesture()) return false val path = uri.encodedPath ?: return false if (path.startsWith("/admin")) return true if (path.startsWith("/content-viewers/")) return true if (Regex("^/sessions/[^/]+/files/[^/]+").containsMatchIn(path)) return true return uri.getQueryParameter("download") == "1" } private fun openExternal(uri: Uri): Boolean { return try { startActivity( Intent(Intent.ACTION_VIEW, uri).apply { addCategory(Intent.CATEGORY_BROWSABLE) } ) true } catch (e: Exception) { false } } private fun openFileChooser(callback: ValueCallback>) { fileChooserCallback = callback val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { val photoFile = createTempImageFile() cameraImageUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", photoFile) it.putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri) } val galleryIntent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*", "application/pdf", "*/*")) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } filePickerLauncher.launch( Intent.createChooser(galleryIntent, getString(R.string.chooser_title)).apply { putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(cameraIntent)) } ) } private fun createTempImageFile(): File { val timestamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) val dir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile("IMG_${timestamp}_", ".jpg", dir) } private fun errorPage(message: String) = """

Не удалось подключиться

$message

При следующем запуске будет запрошен адрес сервера

""".trimIndent() override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) { webView.goBack() return true } return super.onKeyDown(keyCode, event) } }