|
@@ -0,0 +1,562 @@
|
|
|
|
|
+package com.zjznai.safetycheck
|
|
|
|
|
+
|
|
|
|
|
+import android.Manifest
|
|
|
|
|
+import android.app.Activity
|
|
|
|
|
+import android.content.ActivityNotFoundException
|
|
|
|
|
+import android.content.ClipData
|
|
|
|
|
+import android.content.Context
|
|
|
|
|
+import android.content.Intent
|
|
|
|
|
+import android.content.pm.PackageManager
|
|
|
|
|
+import android.graphics.Bitmap
|
|
|
|
|
+import android.net.ConnectivityManager
|
|
|
|
|
+import android.net.Network
|
|
|
|
|
+import android.net.Uri
|
|
|
|
|
+import android.os.Bundle
|
|
|
|
|
+import android.os.Environment
|
|
|
|
|
+import android.provider.MediaStore
|
|
|
|
|
+import android.view.View
|
|
|
|
|
+import android.webkit.CookieManager
|
|
|
|
|
+import android.webkit.PermissionRequest
|
|
|
|
|
+import android.webkit.ValueCallback
|
|
|
|
|
+import android.webkit.WebChromeClient
|
|
|
|
|
+import android.webkit.WebResourceError
|
|
|
|
|
+import android.webkit.WebResourceRequest
|
|
|
|
|
+import android.webkit.WebResourceResponse
|
|
|
|
|
+import android.webkit.WebSettings
|
|
|
|
|
+import android.webkit.WebView
|
|
|
|
|
+import android.webkit.WebViewClient
|
|
|
|
|
+import androidx.activity.OnBackPressedCallback
|
|
|
|
|
+import androidx.activity.result.contract.ActivityResultContracts
|
|
|
|
|
+import androidx.appcompat.app.AppCompatActivity
|
|
|
|
|
+import androidx.core.content.ContextCompat
|
|
|
|
|
+import androidx.core.content.FileProvider
|
|
|
|
|
+import androidx.core.view.WindowCompat
|
|
|
|
|
+import androidx.core.view.WindowInsetsCompat
|
|
|
|
|
+import androidx.core.view.WindowInsetsControllerCompat
|
|
|
|
|
+import com.zjznai.safetycheck.databinding.ActivityMainBinding
|
|
|
|
|
+import java.io.File
|
|
|
|
|
+import java.text.SimpleDateFormat
|
|
|
|
|
+import java.util.Date
|
|
|
|
|
+import java.util.Locale
|
|
|
|
|
+
|
|
|
|
|
+class MainActivity : AppCompatActivity() {
|
|
|
|
|
+
|
|
|
|
|
+ private enum class ConnectionState {
|
|
|
|
|
+ NONE,
|
|
|
|
|
+ NO_NETWORK,
|
|
|
|
|
+ SERVER_UNREACHABLE,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private lateinit var binding: ActivityMainBinding
|
|
|
|
|
+ private val connectivityManager by lazy {
|
|
|
|
|
+ getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private var pendingPermissionRequest: PermissionRequest? = null
|
|
|
|
|
+ private var pendingFilePathCallback: ValueCallback<Array<Uri>>? = null
|
|
|
|
|
+ private var pendingFileChooserParams: WebChromeClient.FileChooserParams? = null
|
|
|
|
|
+ private var pendingCameraImageUri: Uri? = null
|
|
|
|
|
+ private var currentConnectionState = ConnectionState.NONE
|
|
|
|
|
+ private var isPageLoading = false
|
|
|
|
|
+ private var isNetworkCallbackRegistered = false
|
|
|
|
|
+
|
|
|
|
|
+ private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
|
|
|
|
+ override fun onAvailable(network: Network) {
|
|
|
|
|
+ runOnUiThread {
|
|
|
|
|
+ if (currentConnectionState != ConnectionState.NONE && !isPageLoading) {
|
|
|
|
|
+ loadHomePage()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private val mediaPermissionLauncher = registerForActivityResult(
|
|
|
|
|
+ ActivityResultContracts.RequestMultiplePermissions()
|
|
|
|
|
+ ) { permissions ->
|
|
|
|
|
+ val request = pendingPermissionRequest ?: return@registerForActivityResult
|
|
|
|
|
+ pendingPermissionRequest = null
|
|
|
|
|
+
|
|
|
|
|
+ val grantedResources = request.resources.filter { resource ->
|
|
|
|
|
+ when (resource) {
|
|
|
|
|
+ PermissionRequest.RESOURCE_VIDEO_CAPTURE -> permissions[Manifest.permission.CAMERA] == true
|
|
|
|
|
+ PermissionRequest.RESOURCE_AUDIO_CAPTURE -> permissions[Manifest.permission.RECORD_AUDIO] == true
|
|
|
|
|
+ else -> false
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (grantedResources.isNotEmpty()) {
|
|
|
|
|
+ request.grant(grantedResources.toTypedArray())
|
|
|
|
|
+ } else {
|
|
|
|
|
+ request.deny()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private val cameraPermissionLauncher = registerForActivityResult(
|
|
|
|
|
+ ActivityResultContracts.RequestPermission()
|
|
|
|
|
+ ) { granted ->
|
|
|
|
|
+ val fileChooserParams = pendingFileChooserParams ?: return@registerForActivityResult
|
|
|
|
|
+ if (granted) {
|
|
|
|
|
+ launchFileChooser(fileChooserParams, includeCamera = true)
|
|
|
|
|
+ } else if (isCaptureOnlyImageRequest(fileChooserParams)) {
|
|
|
|
|
+ clearPendingFileChooser()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ launchFileChooser(fileChooserParams, includeCamera = false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private val fileChooserLauncher = registerForActivityResult(
|
|
|
|
|
+ ActivityResultContracts.StartActivityForResult()
|
|
|
|
|
+ ) { result ->
|
|
|
|
|
+ val resultUris = when {
|
|
|
|
|
+ result.resultCode != Activity.RESULT_OK -> null
|
|
|
|
|
+ result.data?.clipData != null -> {
|
|
|
|
|
+ val clipData = result.data!!.clipData!!
|
|
|
|
|
+ Array(clipData.itemCount) { index -> clipData.getItemAt(index).uri }
|
|
|
|
|
+ }
|
|
|
|
|
+ result.data?.data != null -> arrayOf(result.data!!.data!!)
|
|
|
|
|
+ pendingCameraImageUri != null -> arrayOf(pendingCameraImageUri!!)
|
|
|
|
|
+ else -> null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ clearPendingFileChooser(resultUris)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
|
+ super.onCreate(savedInstanceState)
|
|
|
|
|
+ binding = ActivityMainBinding.inflate(layoutInflater)
|
|
|
|
|
+ setContentView(binding.root)
|
|
|
|
|
+
|
|
|
|
|
+ enableImmersiveMode()
|
|
|
|
|
+ WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
|
|
|
|
+ configureWebView()
|
|
|
|
|
+ configureOfflineState()
|
|
|
|
|
+ handleBackPress()
|
|
|
|
|
+
|
|
|
|
|
+ if (savedInstanceState == null) {
|
|
|
|
|
+ loadHomePage()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ binding.webView.restoreState(savedInstanceState)
|
|
|
|
|
+ currentConnectionState = savedInstanceState.getString(KEY_CONNECTION_STATE)
|
|
|
|
|
+ ?.let(ConnectionState::valueOf)
|
|
|
|
|
+ ?: ConnectionState.NONE
|
|
|
|
|
+ renderConnectionState(currentConnectionState)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onStart() {
|
|
|
|
|
+ super.onStart()
|
|
|
|
|
+ registerNetworkCallback()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onStop() {
|
|
|
|
|
+ unregisterNetworkCallback()
|
|
|
|
|
+ super.onStop()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun enableImmersiveMode() {
|
|
|
|
|
+ WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
|
|
|
+ WindowCompat.getInsetsController(window, window.decorView).apply {
|
|
|
|
|
+ systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
|
|
|
|
+ hide(WindowInsetsCompat.Type.systemBars())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun configureOfflineState() {
|
|
|
|
|
+ binding.retryButton.setOnClickListener {
|
|
|
|
|
+ loadHomePage()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun configureWebView() {
|
|
|
|
|
+ CookieManager.getInstance().setAcceptCookie(true)
|
|
|
|
|
+ CookieManager.getInstance().setAcceptThirdPartyCookies(binding.webView, true)
|
|
|
|
|
+
|
|
|
|
|
+ binding.webView.apply {
|
|
|
|
|
+ isVerticalScrollBarEnabled = false
|
|
|
|
|
+ isHorizontalScrollBarEnabled = false
|
|
|
|
|
+ overScrollMode = WebView.OVER_SCROLL_NEVER
|
|
|
|
|
+ webChromeClient = object : WebChromeClient() {
|
|
|
|
|
+ override fun onPermissionRequest(request: PermissionRequest) {
|
|
|
|
|
+ runOnUiThread {
|
|
|
|
|
+ handleWebPermissionRequest(request)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onPermissionRequestCanceled(request: PermissionRequest) {
|
|
|
|
|
+ if (pendingPermissionRequest == request) {
|
|
|
|
|
+ pendingPermissionRequest = null
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onShowFileChooser(
|
|
|
|
|
+ webView: WebView?,
|
|
|
|
|
+ filePathCallback: ValueCallback<Array<Uri>>?,
|
|
|
|
|
+ fileChooserParams: FileChooserParams?
|
|
|
|
|
+ ): Boolean {
|
|
|
|
|
+ if (filePathCallback == null || fileChooserParams == null) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ pendingFilePathCallback?.onReceiveValue(null)
|
|
|
|
|
+ pendingFilePathCallback = filePathCallback
|
|
|
|
|
+ pendingFileChooserParams = fileChooserParams
|
|
|
|
|
+
|
|
|
|
|
+ if (shouldOfferCameraCapture(fileChooserParams) && !hasCameraPermission()) {
|
|
|
|
|
+ cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ launchFileChooser(
|
|
|
|
|
+ fileChooserParams = fileChooserParams,
|
|
|
|
|
+ includeCamera = shouldOfferCameraCapture(fileChooserParams)
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ webViewClient = object : WebViewClient() {
|
|
|
|
|
+ override fun shouldOverrideUrlLoading(
|
|
|
|
|
+ view: WebView,
|
|
|
|
|
+ request: WebResourceRequest
|
|
|
|
|
+ ): Boolean {
|
|
|
|
|
+ val uri = request.url
|
|
|
|
|
+ return if (uri.scheme == "http" || uri.scheme == "https") {
|
|
|
|
|
+ false
|
|
|
|
|
+ } else {
|
|
|
|
|
+ openExternalUri(uri)
|
|
|
|
|
+ true
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
|
|
|
|
+ if (url == BLANK_PAGE_URL && currentConnectionState != ConnectionState.NONE) {
|
|
|
|
|
+ binding.loadingIndicator.hide()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ isPageLoading = true
|
|
|
|
|
+ renderConnectionState(ConnectionState.NONE)
|
|
|
|
|
+ binding.loadingIndicator.show()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onReceivedError(
|
|
|
|
|
+ view: WebView?,
|
|
|
|
|
+ request: WebResourceRequest,
|
|
|
|
|
+ error: WebResourceError?
|
|
|
|
|
+ ) {
|
|
|
|
|
+ if (!request.isForMainFrame || request.url.toString() == BLANK_PAGE_URL) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ isPageLoading = false
|
|
|
|
|
+ clearWebViewContent(view)
|
|
|
|
|
+ renderConnectionState(resolveConnectionState())
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onReceivedHttpError(
|
|
|
|
|
+ view: WebView?,
|
|
|
|
|
+ request: WebResourceRequest,
|
|
|
|
|
+ errorResponse: WebResourceResponse
|
|
|
|
|
+ ) {
|
|
|
|
|
+ if (!request.isForMainFrame || errorResponse.statusCode < 400) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ isPageLoading = false
|
|
|
|
|
+ clearWebViewContent(view)
|
|
|
|
|
+ renderConnectionState(ConnectionState.SERVER_UNREACHABLE)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onPageFinished(view: WebView?, url: String?) {
|
|
|
|
|
+ if (url == BLANK_PAGE_URL && currentConnectionState != ConnectionState.NONE) {
|
|
|
|
|
+ binding.loadingIndicator.hide()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ isPageLoading = false
|
|
|
|
|
+ renderConnectionState(ConnectionState.NONE)
|
|
|
|
|
+ binding.loadingIndicator.hide()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ settings.apply {
|
|
|
|
|
+ javaScriptEnabled = true
|
|
|
|
|
+ domStorageEnabled = true
|
|
|
|
|
+ loadsImagesAutomatically = true
|
|
|
|
|
+ mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
|
|
|
|
|
+ mediaPlaybackRequiresUserGesture = false
|
|
|
|
|
+ builtInZoomControls = false
|
|
|
|
|
+ displayZoomControls = false
|
|
|
|
|
+ allowFileAccess = true
|
|
|
|
|
+ allowContentAccess = true
|
|
|
|
|
+ setSupportZoom(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun loadHomePage() {
|
|
|
|
|
+ if (!isNetworkAvailable()) {
|
|
|
|
|
+ isPageLoading = false
|
|
|
|
|
+ clearWebViewContent(binding.webView)
|
|
|
|
|
+ renderConnectionState(ConnectionState.NO_NETWORK)
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ isPageLoading = true
|
|
|
|
|
+ renderConnectionState(ConnectionState.NONE)
|
|
|
|
|
+ binding.loadingIndicator.show()
|
|
|
|
|
+ binding.webView.loadUrl(BuildConfig.START_URL)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun renderConnectionState(state: ConnectionState) {
|
|
|
|
|
+ currentConnectionState = state
|
|
|
|
|
+ val showOffline = state != ConnectionState.NONE
|
|
|
|
|
+
|
|
|
|
|
+ binding.offlineContainer.visibility = if (showOffline) View.VISIBLE else View.GONE
|
|
|
|
|
+ binding.webView.visibility = if (showOffline) View.INVISIBLE else View.VISIBLE
|
|
|
|
|
+
|
|
|
|
|
+ when (state) {
|
|
|
|
|
+ ConnectionState.NONE -> {
|
|
|
|
|
+ binding.offlineHint.visibility = View.GONE
|
|
|
|
|
+ }
|
|
|
|
|
+ ConnectionState.NO_NETWORK -> {
|
|
|
|
|
+ binding.offlineTitle.setText(R.string.offline_no_network_title)
|
|
|
|
|
+ binding.offlineMessage.setText(R.string.offline_no_network_message)
|
|
|
|
|
+ binding.offlineHint.setText(R.string.offline_auto_retry_hint)
|
|
|
|
|
+ binding.offlineHint.visibility = View.VISIBLE
|
|
|
|
|
+ binding.retryButton.setText(R.string.retry)
|
|
|
|
|
+ binding.loadingIndicator.hide()
|
|
|
|
|
+ }
|
|
|
|
|
+ ConnectionState.SERVER_UNREACHABLE -> {
|
|
|
|
|
+ binding.offlineTitle.setText(R.string.offline_server_title)
|
|
|
|
|
+ binding.offlineMessage.setText(R.string.offline_server_message)
|
|
|
|
|
+ binding.offlineHint.visibility = View.GONE
|
|
|
|
|
+ binding.retryButton.setText(R.string.retry)
|
|
|
|
|
+ binding.loadingIndicator.hide()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun resolveConnectionState(): ConnectionState {
|
|
|
|
|
+ return if (isNetworkAvailable()) {
|
|
|
|
|
+ ConnectionState.SERVER_UNREACHABLE
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ConnectionState.NO_NETWORK
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Suppress("DEPRECATION")
|
|
|
|
|
+ private fun isNetworkAvailable(): Boolean {
|
|
|
|
|
+ return connectivityManager.activeNetworkInfo?.isConnected == true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun registerNetworkCallback() {
|
|
|
|
|
+ if (isNetworkCallbackRegistered) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ connectivityManager.registerDefaultNetworkCallback(networkCallback)
|
|
|
|
|
+ isNetworkCallbackRegistered = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun unregisterNetworkCallback() {
|
|
|
|
|
+ if (!isNetworkCallbackRegistered) {
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ connectivityManager.unregisterNetworkCallback(networkCallback)
|
|
|
|
|
+ isNetworkCallbackRegistered = false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun clearWebViewContent(view: WebView?) {
|
|
|
|
|
+ view?.stopLoading()
|
|
|
|
|
+ if (view?.url != BLANK_PAGE_URL) {
|
|
|
|
|
+ view?.loadUrl(BLANK_PAGE_URL)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun handleWebPermissionRequest(request: PermissionRequest) {
|
|
|
|
|
+ pendingPermissionRequest?.deny()
|
|
|
|
|
+ pendingPermissionRequest = request
|
|
|
|
|
+
|
|
|
|
|
+ val permissionsToRequest = buildList {
|
|
|
|
|
+ if (request.resources.contains(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) {
|
|
|
|
|
+ add(Manifest.permission.CAMERA)
|
|
|
|
|
+ }
|
|
|
|
|
+ if (request.resources.contains(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) {
|
|
|
|
|
+ add(Manifest.permission.RECORD_AUDIO)
|
|
|
|
|
+ }
|
|
|
|
|
+ }.distinct()
|
|
|
|
|
+
|
|
|
|
|
+ if (permissionsToRequest.isEmpty()) {
|
|
|
|
|
+ request.deny()
|
|
|
|
|
+ pendingPermissionRequest = null
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val missingPermissions = permissionsToRequest.filterNot { permission ->
|
|
|
|
|
+ ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (missingPermissions.isEmpty()) {
|
|
|
|
|
+ request.grant(request.resources)
|
|
|
|
|
+ pendingPermissionRequest = null
|
|
|
|
|
+ } else {
|
|
|
|
|
+ mediaPermissionLauncher.launch(missingPermissions.toTypedArray())
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun launchFileChooser(
|
|
|
|
|
+ fileChooserParams: WebChromeClient.FileChooserParams,
|
|
|
|
|
+ includeCamera: Boolean
|
|
|
|
|
+ ) {
|
|
|
|
|
+ pendingFileChooserParams = null
|
|
|
|
|
+
|
|
|
|
|
+ val contentIntent = Intent(Intent.ACTION_GET_CONTENT).apply {
|
|
|
|
|
+ addCategory(Intent.CATEGORY_OPENABLE)
|
|
|
|
|
+ type = resolvePrimaryMimeType(fileChooserParams.acceptTypes)
|
|
|
|
|
+ val extraMimeTypes = resolveExtraMimeTypes(fileChooserParams.acceptTypes)
|
|
|
|
|
+ if (extraMimeTypes.isNotEmpty()) {
|
|
|
|
|
+ putExtra(Intent.EXTRA_MIME_TYPES, extraMimeTypes)
|
|
|
|
|
+ }
|
|
|
|
|
+ putExtra(
|
|
|
|
|
+ Intent.EXTRA_ALLOW_MULTIPLE,
|
|
|
|
|
+ fileChooserParams.mode == WebChromeClient.FileChooserParams.MODE_OPEN_MULTIPLE
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val cameraIntent = if (includeCamera) createCameraCaptureIntent() else null
|
|
|
|
|
+ val launchIntent = if (isCaptureOnlyImageRequest(fileChooserParams) && cameraIntent != null) {
|
|
|
|
|
+ cameraIntent
|
|
|
|
|
+ } else {
|
|
|
|
|
+ Intent(Intent.ACTION_CHOOSER).apply {
|
|
|
|
|
+ putExtra(Intent.EXTRA_INTENT, contentIntent)
|
|
|
|
|
+ putExtra(Intent.EXTRA_TITLE, getString(R.string.file_chooser_title))
|
|
|
|
|
+ if (cameraIntent != null) {
|
|
|
|
|
+ putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(cameraIntent))
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ fileChooserLauncher.launch(launchIntent)
|
|
|
|
|
+ } catch (_: ActivityNotFoundException) {
|
|
|
|
|
+ clearPendingFileChooser()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun createCameraCaptureIntent(): Intent? {
|
|
|
|
|
+ val imageFile = createTempImageFile() ?: return null
|
|
|
|
|
+ val imageUri = FileProvider.getUriForFile(this, "$packageName.fileprovider", imageFile)
|
|
|
|
|
+ pendingCameraImageUri = imageUri
|
|
|
|
|
+
|
|
|
|
|
+ val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
|
|
|
|
|
+ if (intent.resolveActivity(packageManager) == null) {
|
|
|
|
|
+ pendingCameraImageUri = null
|
|
|
|
|
+ return null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ val grantFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
|
|
|
|
+ intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri)
|
|
|
|
|
+ intent.clipData = ClipData.newUri(contentResolver, "CapturedImage", imageUri)
|
|
|
|
|
+ intent.addFlags(grantFlags)
|
|
|
|
|
+
|
|
|
|
|
+ packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY).forEach { resolveInfo ->
|
|
|
|
|
+ grantUriPermission(resolveInfo.activityInfo.packageName, imageUri, grantFlags)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return intent
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun createTempImageFile(): File? {
|
|
|
|
|
+ val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
|
|
|
|
+ val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: cacheDir
|
|
|
|
|
+ return runCatching {
|
|
|
|
|
+ File.createTempFile("JPEG_${timeStamp}_", ".jpg", storageDir)
|
|
|
|
|
+ }.getOrNull()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun hasCameraPermission(): Boolean {
|
|
|
|
|
+ return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun shouldOfferCameraCapture(fileChooserParams: WebChromeClient.FileChooserParams): Boolean {
|
|
|
|
|
+ return fileChooserParams.isCaptureEnabled || acceptsImages(fileChooserParams.acceptTypes)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun isCaptureOnlyImageRequest(fileChooserParams: WebChromeClient.FileChooserParams): Boolean {
|
|
|
|
|
+ return fileChooserParams.isCaptureEnabled && acceptsImages(fileChooserParams.acceptTypes)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun acceptsImages(acceptTypes: Array<String>): Boolean {
|
|
|
|
|
+ val types = normalizeAcceptTypes(acceptTypes)
|
|
|
|
|
+ return types.isEmpty() || types.any { it.startsWith("image/") || it == "*/*" }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun resolvePrimaryMimeType(acceptTypes: Array<String>): String {
|
|
|
|
|
+ val types = normalizeAcceptTypes(acceptTypes)
|
|
|
|
|
+ return when {
|
|
|
|
|
+ types.isEmpty() -> "*/*"
|
|
|
|
|
+ types.size == 1 -> types.first()
|
|
|
|
|
+ else -> "*/*"
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun resolveExtraMimeTypes(acceptTypes: Array<String>): Array<String> {
|
|
|
|
|
+ val types = normalizeAcceptTypes(acceptTypes)
|
|
|
|
|
+ return if (types.size > 1) types.toTypedArray() else emptyArray()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun normalizeAcceptTypes(acceptTypes: Array<String>): List<String> {
|
|
|
|
|
+ return acceptTypes
|
|
|
|
|
+ .flatMap { it.split(',') }
|
|
|
|
|
+ .map { it.trim() }
|
|
|
|
|
+ .filter { it.isNotEmpty() }
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun clearPendingFileChooser(resultUris: Array<Uri>? = null) {
|
|
|
|
|
+ pendingFilePathCallback?.onReceiveValue(resultUris)
|
|
|
|
|
+ pendingFilePathCallback = null
|
|
|
|
|
+ pendingFileChooserParams = null
|
|
|
|
|
+ pendingCameraImageUri = null
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun handleBackPress() {
|
|
|
|
|
+ onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
|
|
|
|
+ override fun handleOnBackPressed() {
|
|
|
|
|
+ if (binding.webView.canGoBack()) {
|
|
|
|
|
+ binding.webView.goBack()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ finish()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private fun openExternalUri(uri: Uri) {
|
|
|
|
|
+ val intent = Intent(Intent.ACTION_VIEW, uri)
|
|
|
|
|
+ try {
|
|
|
|
|
+ startActivity(intent)
|
|
|
|
|
+ } catch (_: ActivityNotFoundException) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onWindowFocusChanged(hasFocus: Boolean) {
|
|
|
|
|
+ super.onWindowFocusChanged(hasFocus)
|
|
|
|
|
+ if (hasFocus) {
|
|
|
|
|
+ enableImmersiveMode()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onSaveInstanceState(outState: Bundle) {
|
|
|
|
|
+ outState.putString(KEY_CONNECTION_STATE, currentConnectionState.name)
|
|
|
|
|
+ binding.webView.saveState(outState)
|
|
|
|
|
+ super.onSaveInstanceState(outState)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ override fun onDestroy() {
|
|
|
|
|
+ pendingPermissionRequest?.deny()
|
|
|
|
|
+ pendingPermissionRequest = null
|
|
|
|
|
+ clearPendingFileChooser()
|
|
|
|
|
+ binding.webView.destroy()
|
|
|
|
|
+ super.onDestroy()
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ companion object {
|
|
|
|
|
+ private const val BLANK_PAGE_URL = "about:blank"
|
|
|
|
|
+ private const val KEY_CONNECTION_STATE = "connection_state"
|
|
|
|
|
+ }
|
|
|
|
|
+}
|