Issue
I have a webform that's fairly simple. The only people accessing the webform are using mobile devices so I built the form with mobile in use.
To make it simpler for the user, I have two file upload buttons (the HTML here isn't too important other than to explain what I'm trying to do and why I haven't been able to resolve it). One upload button is a simple upload button where the user would navigate their files. The second upload button should open up the camera straight away.
This is working fine when I open the form on a mobile browser.
<div class = "col-6-6">
<label class = "image-label label-upload">
<span class = "icons i-upload"></span>
Upload
<input name="image-upload" type = "file" accept="image/*">
</label>
</div>
<div class = "col-6-6">
<label class = "image-label label-upload">
<span class = "icons i-camera"></span>
Capture
<input name="image-capture" type = "file" accept="image/*" capture = "environment">
</label>
</div>
What I'm now attempting to do is simulate the exact same behavior the browser has as an App using webview.
package com.example.testapp
import android.os.Bundle
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
supportActionBar?.hide()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val myWebView: WebView = findViewById(R.id.wv)
myWebView.loadUrl("https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture")
myWebView.webViewClient = WebViewClient()
myWebView.webChromeClient = WebChromeClient()
myWebView.settings.javaScriptEnabled = true
myWebView.settings.allowFileAccess = true
myWebView.settings.allowContentAccess = true
myWebView.settings.javaScriptCanOpenWindowsAutomatically = true
myWebView.settings.mediaPlaybackRequiresUserGesture = false
}
override fun onBackPressed() {
val myWebView: WebView = findViewById(R.id.wv)
myWebView.webViewClient = WebViewClient()
if (myWebView.canGoBack()) myWebView.goBack() else super.onBackPressed()
}
}
I also have the following in my Android manifest:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.CAMERA2" />
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-feature android:name="android.hardware.camera" android:required="true"/>
I've seen some solutions online and tried them out but they don't seem to work exactly how I'm expecting them to...
Solutions are often in Java, trying to do this in Kotlin. As for the Kotlin solutions I've tried out, they either don't give enough information (so unable to understand), it defaults to just choosing an image (no camera option), you get a pop-up menu asking if you want to use the camera or file explorer (it should know which one to used based off of the HTML use of capture), or it will have both options and the camera option doesn't work properly.
Also, should be said that all users of this app are on Android 8 and higher. So there's no need for legacy solutions.
Any and all help would be greatly appreciated. I'm trying to keep it simple, not sure if there's something already built in that can be accessed to make this happen.
Regards,
Alex
Solution
I figured it out! Actually took a bit of playing around with some other solutions and asking for some external help.
Under AndroidManifest.xml I added the following inside <Application>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
Then I created a new xml file (app > res > xml), file_paths.xml
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path
name="camera_files"
path="DCIM/Camera" />
</paths>
And finally... my updated Kotlin file (MainActivity.kt)
package com.example.testapp
import android.Manifest
import android.content.ContentResolver
import android.content.ContentValues
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.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : AppCompatActivity() {
private lateinit var myWebView: WebView
private var fileUploadCallback: ValueCallback<Array<Uri>>? = null
private lateinit var currentPhotoUri: Uri
companion object {
private const val FILE_CHOOSER_REQUEST_CODE = 1
private const val CAMERA_PERMISSION_REQUEST_CODE = 2
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
myWebView = findViewById(R.id.wv)
myWebView.loadUrl("https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture")
myWebView.webViewClient = WebViewClient()
myWebView.webChromeClient = object : WebChromeClient() {
override fun onShowFileChooser(
webView: WebView?,
filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams?
): Boolean {
fileUploadCallback?.onReceiveValue(null)
fileUploadCallback = filePathCallback
if (fileChooserParams?.acceptTypes?.contains("image/*") == true && fileChooserParams.isCaptureEnabled) {
// Launch camera
if (ContextCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
launchCamera()
} else {
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_REQUEST_CODE
)
}
} else {
// Use file picker
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.type = "image/*"
val chooserIntent = Intent.createChooser(intent, "Choose File")
startActivityForResult(chooserIntent, FILE_CHOOSER_REQUEST_CODE)
}
return true
}
}
val webSettings: WebSettings = myWebView.settings
with(webSettings) {
javaScriptEnabled = true
allowFileAccess = true
allowContentAccess = true
javaScriptCanOpenWindowsAutomatically = true
mediaPlaybackRequiresUserGesture = false
domStorageEnabled = true
}
}
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) {
myWebView.goBack()
return true
}
return super.onKeyDown(keyCode, event)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == FILE_CHOOSER_REQUEST_CODE) {
if (fileUploadCallback == null) {
super.onActivityResult(requestCode, resultCode, data)
return
}
val results: Array<Uri>? = when {
resultCode == RESULT_OK && data?.data != null -> arrayOf(data.data!!)
resultCode == RESULT_OK -> arrayOf(currentPhotoUri)
else -> null
}
fileUploadCallback?.onReceiveValue(results)
fileUploadCallback = null
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// Permission granted, launch camera
launchCamera()
} else {
// Permission denied, show an error or request permission again
Toast.makeText(this, "Camera permission denied", Toast.LENGTH_SHORT).show()
}
}
}
private fun launchCamera() {
val captureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
currentPhotoUri = createImageFileUri()
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, currentPhotoUri)
startActivityForResult(captureIntent, FILE_CHOOSER_REQUEST_CODE)
}
private fun createImageFileUri(): Uri {
val fileName = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date()) + ".jpg"
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM)
}
}
val resolver: ContentResolver = contentResolver
val imageUri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
return imageUri ?: throw RuntimeException("ImageUri is null")
}
}
It's not as "simple" as I had hoped it would be. But it works! Also it does save the picture to the cameraroll, I believe it's also possible to save to temporary storage by using something like applicationContext.externalCacheDir
.
I hope this saves someone from having a headache, as for me. I am done with this and don't want to think about it any longer lol.
Alex
Answered By - Alex
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.