码桶

发现社区成员的开源项目

MainActivity.kt10.4 KB
package com.dalao.forum

import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.provider.MediaStore
import android.view.Menu
import android.view.MenuItem
import android.webkit.WebChromeClient
import android.webkit.WebView
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import com.dalao.forum.databinding.ActivityMainBinding
import java.io.File

class MainActivity : AppCompatActivity() {

    companion object {
        const val TARGET_URL = "https://www.dalao.net/"
        private const val REQUEST_CAMERA_PERMISSION = 100
        private const val REQUEST_NOTIFICATION_PERMISSION = 101
    }

    private lateinit var binding: ActivityMainBinding
    private var fileChooserCallback: android.webkit.ValueCallback<Array<Uri>>? = null
    private var cameraImageUri: Uri? = null

    private val fileChooserLauncher = registerForActivityResult(
        androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
    ) { result ->
        handleFileChooserResult(result.resultCode, result.data)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        setTheme(ThemeManager.getSavedTheme(this).styleRes)
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        setSupportActionBar(binding.toolbar)
        setupWebView()
        setupSwipeRefresh()
        setupErrorRetry()

        NotificationHelper.createChannels(this)
        ForumSyncScheduler.ensureScheduled(this)
        requestNotificationPermissionIfNeeded()

        if (savedInstanceState == null) {
            binding.webView.loadUrl(TARGET_URL)
        }

        onBackPressedDispatcher.addCallback(this) {
            if (binding.webView.canGoBack()) {
                binding.webView.goBack()
            } else {
                isEnabled = false
                onBackPressedDispatcher.onBackPressed()
            }
        }
    }

    private fun requestNotificationPermissionIfNeeded() {
        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.TIRAMISU &&
            checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) !=
            PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(
                arrayOf(Manifest.permission.POST_NOTIFICATIONS),
                REQUEST_NOTIFICATION_PERMISSION
            )
        }
    }

    private fun setupWebView() {
        val webView = binding.webView
        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true
            allowFileAccess = false
            mixedContentMode = android.webkit.WebSettings.MIXED_CONTENT_NEVER_ALLOW
            cacheMode = android.webkit.WebSettings.LOAD_DEFAULT
            setSupportZoom(true)
            builtInZoomControls = false
        }

        webView.webViewClient = object : android.webkit.WebViewClient() {
            override fun shouldOverrideUrlLoading(
                view: WebView,
                request: android.webkit.WebResourceRequest
            ): Boolean {
                val url = request.url.toString()
                return if (url.startsWith("http://") || url.startsWith("https://")) {
                    false
                } else {
                    try {
                        startActivity(Intent(Intent.ACTION_VIEW, request.url))
                    } catch (_: Exception) { }
                    true
                }
            }

            override fun onPageFinished(view: WebView, url: String?) {
                binding.swipeRefresh.isRefreshing = false
                binding.errorView.visibility = android.view.View.GONE
                binding.swipeRefresh.visibility = android.view.View.VISIBLE
            }

            override fun onReceivedError(
                view: WebView,
                request: android.webkit.WebResourceRequest,
                error: android.webkit.WebResourceError
            ) {
                if (request.isForMainFrame) {
                    binding.swipeRefresh.isRefreshing = false
                    binding.errorView.visibility = android.view.View.VISIBLE
                    binding.swipeRefresh.visibility = android.view.View.GONE
                }
            }
        }

        webView.webChromeClient = object : WebChromeClient() {
            override fun onProgressChanged(view: WebView, newProgress: Int) {
                binding.progressBar.progress = newProgress
                binding.progressBar.visibility =
                    if (newProgress in 1..99) android.view.View.VISIBLE else android.view.View.GONE
            }

            override fun onShowFileChooser(
                webView: WebView,
                filePathCallback: android.webkit.ValueCallback<Array<Uri>>,
                fileChooserParams: FileChooserParams
            ): Boolean {
                fileChooserCallback?.onReceiveValue(null)
                fileChooserCallback = filePathCallback
                launchFileChooser()
                return true
            }
        }
    }

    private fun setupSwipeRefresh() {
        binding.swipeRefresh.setOnRefreshListener {
            binding.webView.reload()
        }
    }

    private fun setupErrorRetry() {
        binding.retryButton.setOnClickListener {
            binding.errorView.visibility = android.view.View.GONE
            binding.swipeRefresh.visibility = android.view.View.VISIBLE
            binding.webView.loadUrl(TARGET_URL)
        }
    }

    private fun launchFileChooser() {
        if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) &&
            checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
        ) {
            requestPermissions(arrayOf(Manifest.permission.CAMERA), REQUEST_CAMERA_PERMISSION)
            return
        }
        openChooserIntent()
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CAMERA_PERMISSION) {
            openChooserIntent()
        }
    }

    private fun openChooserIntent() {
        val intents = mutableListOf<Intent>()

        val cameraGranted =
            checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
        try {
            val picturesDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
            if (cameraGranted && picturesDir != null &&
                packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
            ) {
                picturesDir.mkdirs()
                val photoFile = File(picturesDir, "capture_${System.currentTimeMillis()}.jpg")
                cameraImageUri = FileProvider.getUriForFile(
                    this, "$packageName.fileprovider", photoFile
                )
                val cameraIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
                    putExtra(MediaStore.EXTRA_OUTPUT, cameraImageUri)
                    addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
                }
                if (cameraIntent.resolveActivity(packageManager) != null) {
                    intents.add(cameraIntent)
                }
            }
        } catch (e: Exception) {
            // 相机意图构造失败不应影响相册/文件选择
            cameraImageUri = null
        }

        val contentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
            type = "*/*"
            addCategory(Intent.CATEGORY_OPENABLE)
        }

        val chooserIntent = Intent.createChooser(contentIntent, "选择文件").apply {
            if (intents.isNotEmpty()) {
                putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toTypedArray())
            }
        }
        try {
            fileChooserLauncher.launch(chooserIntent)
        } catch (e: Exception) {
            // 没有可用的文件/相机应用时,回收回调,避免网页文件输入卡死
            fileChooserCallback?.onReceiveValue(null)
            fileChooserCallback = null
            cameraImageUri = null
        }
    }

    private fun handleFileChooserResult(resultCode: Int, data: Intent?) {
        var results: Array<Uri>? = null
        if (resultCode == Activity.RESULT_OK) {
            if (data == null || data.data == null) {
                cameraImageUri?.let { results = arrayOf(it) }
            } else {
                data.data?.let { results = arrayOf(it) }
            }
        }
        fileChooserCallback?.onReceiveValue(results)
        fileChooserCallback = null
        cameraImageUri = null
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_main, menu)
        return true
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        when (item.itemId) {
            R.id.action_refresh -> binding.webView.reload()
            R.id.theme_dark_gold -> applyTheme(AppTheme.DARK_GOLD)
            R.id.theme_light_blue -> applyTheme(AppTheme.LIGHT_BLUE)
            R.id.theme_anime_pink -> applyTheme(AppTheme.ANIME_PINK)
        }
        return super.onOptionsItemSelected(item)
    }

    private fun applyTheme(theme: AppTheme) {
        if (theme == ThemeManager.getSavedTheme(this)) return
        ThemeManager.saveTheme(this, theme)
        // recreate() 在部分机型不重刷主题,改为彻底重启进程内 Activity 栈保证生效
        val intent = Intent(this, MainActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
        finish()
        startActivity(intent)
        overridePendingTransition(0, 0)
    }

    override fun onPause() {
        super.onPause()
        binding.webView.onPause()
        // 持久化 Cookie,供后台轮询 Worker 复用登录态
        android.webkit.CookieManager.getInstance().flush()
    }

    override fun onResume() {
        super.onResume()
        binding.webView.onResume()
    }

    override fun onDestroy() {
        // 正确释放 WebView,避免泄漏与偶发闪退
        binding.webView.apply {
            stopLoading()
            (parent as? android.view.ViewGroup)?.removeView(this)
            removeAllViews()
            destroy()
        }
        super.onDestroy()
    }
}