Newer
Older
navi-1 / android-client / app / src / main / java / com / navi / client / MainActivity.kt
@Eugene Sukhodolskiy Eugene Sukhodolskiy on 22 Apr 6 KB Add Android WebView client (android-client/)
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.provider.MediaStore
import android.view.KeyEvent
import android.webkit.*
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 fileChooserCallback: ValueCallback<Array<Uri>>? = null
    private var cameraImageUri: Uri? = null
    private var pendingFileChooserCallback: ValueCallback<Array<Uri>>? = null

    private val permissionLauncher = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { _ ->
        pendingFileChooserCallback?.let { cb ->
            pendingFileChooserCallback = null
            openFileChooser(cb)
        }
    }

    private val filePickerLauncher = registerForActivityResult(
        ActivityResultContracts.StartActivityForResult()
    ) { result ->
        val uris: Array<Uri>? = 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)

        if (serverUrl.isNullOrBlank()) {
            startActivity(Intent(this, SetupActivity::class.java))
            finish()
            return
        }

        setContentView(R.layout.activity_main)
        webView = findViewById(R.id.webview)
        setupWebView(prefs)
        webView.loadUrl(serverUrl)
    }

    @SuppressLint("SetJavaScriptEnabled")
    private fun setupWebView(prefs: android.content.SharedPreferences) {
        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            cacheMode = WebSettings.LOAD_NO_CACHE
            mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
            mediaPlaybackRequiresUserGesture = false
            allowFileAccess = true
            allowContentAccess = true
        }

        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest) = 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 onShowFileChooser(
                view: WebView,
                callback: ValueCallback<Array<Uri>>,
                params: FileChooserParams
            ): Boolean {
                fileChooserCallback?.onReceiveValue(null)
                fileChooserCallback = callback

                val needed = mutableListOf<String>().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 openFileChooser(callback: ValueCallback<Array<Uri>>) {
        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) = """
        <html><body style="background:#111;color:#888;font-family:sans-serif;padding:40px;text-align:center">
        <h2 style="color:#555">Не удалось подключиться</h2>
        <p>$message</p>
        <p style="font-size:12px">При следующем запуске будет запрошен адрес сервера</p>
        </body></html>
    """.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)
    }
}