diff --git a/app/build.gradle b/app/build.gradle index 2f75ab8..7f63033 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -48,7 +48,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - def composeBom = platform("androidx.compose:compose-bom:2025.04.00") + def composeBom = platform("androidx.compose:compose-bom:$composeBomVersion") implementation composeBom implementation 'androidx.compose.material3:material3' implementation 'androidx.activity:activity-compose' diff --git a/gradle.properties b/gradle.properties index 23339e0..e96ff85 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,3 +19,6 @@ android.useAndroidX=true android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official + +composeBomVersion=2025.04.00 +navigationComposeVersion=2.8.9 \ No newline at end of file diff --git a/webapp/build.gradle b/webapp/build.gradle index 2a02abe..091b772 100644 --- a/webapp/build.gradle +++ b/webapp/build.gradle @@ -1,15 +1,16 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' +apply plugin: 'org.jetbrains.kotlin.plugin.compose' android { namespace "com.yoti.mobile.android.sdk.yotidocscan.websample" - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.yoti.mobile.android.sdk.yotidocscan.websample" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -24,6 +25,7 @@ android { buildFeatures { viewBinding true buildConfig true + compose true } buildTypes { @@ -38,12 +40,12 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.3.0' - implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - implementation 'com.google.android.material:material:1.1.0' - - // Multi-module projects: Add this dependency because of Android Studio Issue - // and androix.navigation dependencies management https://issuetracker.google.com/issues/152245564 - implementation 'androidx.navigation:navigation-ui-ktx:2.2.2' + + def composeBom = platform("androidx.compose:compose-bom:$composeBomVersion") + implementation composeBom + implementation 'androidx.compose.material3:material3' + implementation 'androidx.activity:activity-compose' + debugImplementation 'androidx.compose.ui:ui-tooling' + + implementation "androidx.navigation:navigation-compose:$navigationComposeVersion" } diff --git a/webapp/src/main/AndroidManifest.xml b/webapp/src/main/AndroidManifest.xml index e41b2ec..4a7a81d 100644 --- a/webapp/src/main/AndroidManifest.xml +++ b/webapp/src/main/AndroidManifest.xml @@ -3,8 +3,6 @@ - - diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt deleted file mode 100644 index c82accc..0000000 --- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AccelerometerListener.kt +++ /dev/null @@ -1,61 +0,0 @@ -package com.yoti.mobile.android.sdk.yotidocscan.websample - -import android.content.Context -import android.hardware.Sensor -import android.hardware.Sensor.TYPE_ACCELEROMETER -import android.hardware.SensorEvent -import android.hardware.SensorEventListener -import android.hardware.SensorManager -import android.hardware.SensorManager.SENSOR_DELAY_NORMAL -import android.util.Log -import kotlin.math.sqrt - -private const val TAG = "AccelerometerListener" - -class AccelerometerListener( - private val context: Context, - private val shakeListener: ShakeListener? -) : SensorEventListener { - - interface ShakeListener { - fun onShake() - } - - private var sensorManager: SensorManager? = null - private var shake = 0f - private var currentShake = 0f - private var lastShake = 0f - - fun start() { - sensorManager = (context.getSystemService(Context.SENSOR_SERVICE) as? SensorManager) - sensorManager?.getDefaultSensor(TYPE_ACCELEROMETER)?.let { accelerometerSensor -> - Log.d(TAG, "Start: register SensorManager listener") - sensorManager?.registerListener(this, accelerometerSensor, SENSOR_DELAY_NORMAL) - }?:run { Log.d(TAG, "Accelerometer sensor is not available") } - } - - fun stop() { - Log.d(TAG, "Stop: unregister SensorManager listener") - sensorManager?.unregisterListener(this) - } - - override fun onSensorChanged(event: SensorEvent?) { - event?.run { - val x = values[0] - val y = values[1] - val z = values[2] - lastShake = currentShake - currentShake = sqrt((x * x + y * y + z * z)) - val delta = currentShake - lastShake - shake = shake * 0.9f + delta - if (shake > 12) { - Log.d(TAG, "Shake detected") - shakeListener?.onShake() - } - } - } - - override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) { - // Nothing to do here - } -} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt new file mode 100644 index 0000000..5f02fad --- /dev/null +++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/AppDestinations.kt @@ -0,0 +1,7 @@ +package com.yoti.mobile.android.sdk.yotidocscan.websample + +object AppDestinations { + + const val MAIN_SCREEN = "main_screen" + const val WEB_SCREEN = "web_screen" +} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt index 088924a..01871dc 100644 --- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt +++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainActivity.kt @@ -1,29 +1,35 @@ package com.yoti.mobile.android.sdk.yotidocscan.websample import android.Manifest.permission -import android.annotation.SuppressLint -import android.app.Activity import android.content.Intent -import android.content.pm.PackageManager +import android.graphics.Color.TRANSPARENT import android.net.Uri import android.os.Bundle import android.os.Environment import android.provider.MediaStore -import android.view.View.VISIBLE -import android.webkit.PermissionRequest import android.webkit.ValueCallback -import android.webkit.WebChromeClient import android.webkit.WebChromeClient.FileChooserParams -import android.webkit.WebView -import android.webkit.WebViewClient -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.app.ActivityCompat -import androidx.core.content.ContextCompat +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.activity.result.ActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.core.content.FileProvider -import com.yoti.mobile.android.sdk.yotidocscan.websample.AccelerometerListener.ShakeListener -import com.yoti.mobile.android.sdk.yotidocscan.websample.SessionBottomSheet.SessionConfigurationListener -import com.yoti.mobile.android.sdk.yotidocscan.websample.databinding.ActivityMainBinding +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import com.yoti.mobile.android.sdk.yotidocscan.websample.ui.YotiDocScanWebSampleAppTheme import java.io.File import java.text.SimpleDateFormat import java.util.Date @@ -63,24 +69,14 @@ import java.util.Locale * - Use a NoActionBar theme * - Manage back navigation: users can press back hardware button and exit from the flow */ -private const val CAPTURE_REQUEST_CODE = 1112 -private const val PERMISSIONS_REQUEST_CODE = 1114 - private const val KEY_IS_VIEW_RECREATED = "MainActivity.KEY_IS_VIEW_RECREATED" private const val FINISH_SESSION_URL = "https://www.yoti.com/" -class MainActivity : AppCompatActivity(), SessionConfigurationListener { - - private var sessionBottomSheet: SessionBottomSheet? = null +class MainActivity : ComponentActivity() { private lateinit var cameraCaptureFileUri: Uri private var filePathCallback: ValueCallback>? = null private var isViewRecreated: Boolean = false - private val shakeListener = AccelerometerListener(this, object: ShakeListener { - override fun onShake() { - showOptionsDialog() - } - }) private val mimeTypeMap = mapOf( ".pdf" to "application/pdf", @@ -88,129 +84,111 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener { ".jpg" to "image/jpeg" ) - private lateinit var binding: ActivityMainBinding - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - - binding = ActivityMainBinding.inflate(layoutInflater) - setContentView(binding.root) + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light( + scrim = TRANSPARENT, + darkScrim = TRANSPARENT + ), + navigationBarStyle = SystemBarStyle.light( + scrim = TRANSPARENT, + darkScrim = TRANSPARENT + ) + ) isViewRecreated = savedInstanceState?.getBoolean(KEY_IS_VIEW_RECREATED) ?: false - requestPermissions() - - binding.webview.configureForYdsWeb() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) + setContent { + val navController = rememberNavController() + var sessionUrl by rememberSaveable { mutableStateOf("") } + var showMissingPermissionsDialog by remember { mutableStateOf(false) } + var showSessionFinishedDialog by remember { mutableStateOf(false) } - if (requestCode == CAPTURE_REQUEST_CODE) { - if (!isViewRecreated && resultCode == Activity.RESULT_OK) { - val resultUri = data?.data ?: cameraCaptureFileUri - filePathCallback?.onReceiveValue(arrayOf(resultUri)) - } else { - filePathCallback?.onReceiveValue(null) + val permissionsLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions() + ) { permissions -> + showMissingPermissionsDialog = !(permissions.values.all { it }) + } + LaunchedEffect(Unit) { + permissionsLauncher.launch(arrayOf(permission.CAMERA, permission.RECORD_AUDIO)) } - } - - } - - override fun onResume() { - super.onResume() - shakeListener.start() - } - - override fun onPause() { - shakeListener.stop() - super.onPause() - } - - override fun onDestroy() { - binding.webview.destroy() - super.onDestroy() - } - override fun onRequestPermissionsResult( - requestCode: Int, permissions: Array, grantResults: IntArray - ) { - if (requestCode == PERMISSIONS_REQUEST_CODE) { - grantResults.firstOrNull { it != PackageManager.PERMISSION_GRANTED }?.let { - AlertDialog.Builder(this) - .setTitle("Permissions needed") - .setMessage("All permissions are needed to continue with the YDS session") - .setPositiveButton("OK") { _, _ -> super.finish() } - .show() + val cameraAndFilePickerChooserLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> handleCameraAndFilePickerChooserResult(result) } + + YotiDocScanWebSampleAppTheme { + Scaffold { innerPadding -> + NavHost( + navController = navController, + startDestination = AppDestinations.MAIN_SCREEN, + modifier = Modifier.padding(innerPadding) + ) { + composable(route = AppDestinations.MAIN_SCREEN) { + MainScreen( + sessionUrl = sessionUrl, + showMissingPermissionsDialog = showMissingPermissionsDialog, + onSessionUrlChanged = { sessionUrl = it }, + onStartSessionClicked = { + navController.navigate(AppDestinations.WEB_SCREEN) + }, + onMissingPermissionsConfirmed = { finish() } + ) + } + + composable(route = AppDestinations.WEB_SCREEN) { + WebScreen( + sessionUrl = sessionUrl, + showSessionFinishedDialog = showSessionFinishedDialog, + onPageCommitVisible = { url -> + // Detect the URL that indicates that flow is finished and close the app + if (url == FINISH_SESSION_URL) { + showSessionFinishedDialog = true + } + }, + onShowCameraAndFilePickerChooser = { callback, fileChooserParams -> + filePathCallback = callback + val intent = createCameraAndFilePickerChooserIntent( + fileChooserParams + ) + cameraAndFilePickerChooserLauncher.launch(intent) + }, + onCloseSession = { navController.popBackStack() }, + onSessionFinished = { finish() } + ) + } + } + } } } } - override fun onBackPressed() { - AlertDialog.Builder(this) - .setTitle("Close YDS Session") - .setMessage("Are you sure you want to finish YDS session?") - .setPositiveButton("Yes") { _, _ -> super.onBackPressed() } - .setNegativeButton("No", null) - .show() - } - override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putBoolean(KEY_IS_VIEW_RECREATED, true) } - private fun requestPermissions() { - val permissions = listOf( - permission.CAMERA, - permission.RECORD_AUDIO, - permission.READ_EXTERNAL_STORAGE, - permission.WRITE_EXTERNAL_STORAGE - ) - - val permissionsRequest = permissions.mapNotNull { permission -> - if (ContextCompat.checkSelfPermission(this, permission) != PackageManager.PERMISSION_GRANTED) { - permission - } else null - }.toTypedArray() - - if (permissionsRequest.isNotEmpty()) { - ActivityCompat.requestPermissions( - this, - permissionsRequest, - PERMISSIONS_REQUEST_CODE - ) + private fun handleCameraAndFilePickerChooserResult(result: ActivityResult) { + if (!isViewRecreated && result.resultCode == RESULT_OK) { + val resultUri = result.data?.data ?: cameraCaptureFileUri + filePathCallback?.onReceiveValue(arrayOf(resultUri)) + } else { + filePathCallback?.onReceiveValue(null) } } - @SuppressLint("SetJavaScriptEnabled") - private fun WebView.configureForYdsWeb() { - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) - - this.settings.apply { - javaScriptEnabled = true - allowFileAccess = true - allowUniversalAccessFromFileURLs = true - domStorageEnabled = true - javaScriptCanOpenWindowsAutomatically = true - mediaPlaybackRequiresUserGesture = false - } - this.webViewClient = YdsWebClient() - this.webChromeClient = YdsWebChromeClient() - } - - private fun showCameraAndFilePickerChooser(fileChooserParams: FileChooserParams) { - + private fun createCameraAndFilePickerChooserIntent(fileChooserParams: FileChooserParams): Intent { cameraCaptureFileUri = createFileUri() - - Intent.createChooser(createFilePickerIntent(fileChooserParams), fileChooserParams.title) - .run { - putExtra( - Intent.EXTRA_INITIAL_INTENTS, - listOf(createCameraIntent(cameraCaptureFileUri)).toTypedArray() - ) - startActivityForResult(this, CAPTURE_REQUEST_CODE) - } + return Intent.createChooser( + createFilePickerIntent(fileChooserParams), + fileChooserParams.title + ).also { + it.putExtra( + Intent.EXTRA_INITIAL_INTENTS, + listOf(createCameraIntent(cameraCaptureFileUri)).toTypedArray() + ) + } } private fun createFilePickerIntent(params: FileChooserParams): Intent? { @@ -246,64 +224,4 @@ class MainActivity : AppCompatActivity(), SessionConfigurationListener { File.createTempFile(imageFileName, ".jpg", storageDir) ) } - - private inner class YdsWebChromeClient: WebChromeClient() { - // Launch file picker or camera intent and set the - // filePathCallback to set the capture results to the webview - override fun onShowFileChooser( - webView: WebView?, - filePathCallback: ValueCallback>?, - fileChooserParams: FileChooserParams? - ): Boolean { - this@MainActivity.filePathCallback = filePathCallback - - return if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN) { - showCameraAndFilePickerChooser(fileChooserParams) - true - } else { - false - } - } - - // Grant permission requested - override fun onPermissionRequest(request: PermissionRequest?) { - request?.grant(request.resources) - } - } - - private inner class YdsWebClient : WebViewClient() { - // Detect the URL that indicates that YDS flow is finished - // and close the app - override fun onPageCommitVisible(view: WebView?, url: String?) { - super.onPageCommitVisible(view, url) - if (url == FINISH_SESSION_URL) { - AlertDialog.Builder(this@MainActivity) - .setTitle("YDS Session") - .setMessage("Session finished") - .setPositiveButton("OK") { _, _ -> this@MainActivity.finish() } - .show() - } - } - } - - private fun showOptionsDialog() { - if (sessionBottomSheet != null) return - - sessionBottomSheet = SessionBottomSheet.newInstance() - sessionBottomSheet?.show( - supportFragmentManager, - SessionBottomSheet.FRAGMENT_TAG - ) - } - - override fun onSessionConfigurationSuccess(sessionUrl: String) { - with(binding) { - webview.visibility = VISIBLE - webview.loadUrl(sessionUrl) - } - } - - override fun onSessionConfigurationDismiss() { - sessionBottomSheet = null - } -} +} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt new file mode 100644 index 0000000..b9eb20c --- /dev/null +++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/MainScreen.kt @@ -0,0 +1,122 @@ +package com.yoti.mobile.android.sdk.yotidocscan.websample + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.yoti.mobile.android.sdk.yotidocscan.websample.ui.YotiDocScanWebSampleAppTheme + +@Composable +fun MainScreen( + sessionUrl: String, + showMissingPermissionsDialog: Boolean, + onSessionUrlChanged: (String) -> Unit, + onStartSessionClicked: () -> Unit, + onMissingPermissionsConfirmed: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + OutlinedTextField( + value = sessionUrl, + onValueChange = onSessionUrlChanged, + label = { + Text(text = stringResource(id = R.string.session_url_hint)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Done + ), + modifier = Modifier.fillMaxWidth() + ) + Spacer(Modifier.size(16.dp)) + Button( + onClick = onStartSessionClicked, + enabled = sessionUrl.isNotBlank() + ) { + Text(text = stringResource(id = R.string.start_session_button)) + } + + if (showMissingPermissionsDialog) { + MissingPermissionsDialog { onMissingPermissionsConfirmed() } + } + } +} + +@Composable +private fun MissingPermissionsDialog(onConfirm: () -> Unit) { + AlertDialog( + title = { Text(stringResource(id = R.string.missing_permissions_dialog_title)) }, + text = { Text(stringResource(id = R.string.missing_permissions_dialog_text)) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(id = R.string.missing_permissions_dialog_confirm_button)) + } + }, + onDismissRequest = {} + ) +} + +@Preview(showBackground = true) +@Composable +fun PreviewEmptyMainScreen() { + YotiDocScanWebSampleAppTheme { + MainScreen( + sessionUrl = "", + showMissingPermissionsDialog = false, + onSessionUrlChanged = {}, + onStartSessionClicked = {}, + onMissingPermissionsConfirmed = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewMainScreen() { + YotiDocScanWebSampleAppTheme { + MainScreen( + sessionUrl = "https://example.com/session", + showMissingPermissionsDialog = false, + onSessionUrlChanged = {}, + onStartSessionClicked = {}, + onMissingPermissionsConfirmed = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun PreviewMainScreenWithMissingPermissionsDialog() { + YotiDocScanWebSampleAppTheme { + MainScreen( + sessionUrl = "", + showMissingPermissionsDialog = true, + onSessionUrlChanged = {}, + onStartSessionClicked = {}, + onMissingPermissionsConfirmed = {} + ) + } +} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt deleted file mode 100644 index 419175c..0000000 --- a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/SessionBottomSheet.kt +++ /dev/null @@ -1,79 +0,0 @@ -package com.yoti.mobile.android.sdk.yotidocscan.websample - - -import android.content.Context -import android.content.DialogInterface -import android.content.SharedPreferences -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import com.yoti.mobile.android.sdk.yotidocscan.websample.databinding.BottomSheetBinding - -private const val SESSION_PREFERENCES_ID = "SESSION_PREFERENCES_ID" -private const val SESSION_CONFIGURATION_KEY = "SESSION_CONFIGURATION_KEY" - -class SessionBottomSheet: BottomSheetDialogFragment() { - - private var sessionConfigurationListener: SessionConfigurationListener? = null - lateinit var sharedPreferences: SharedPreferences - - private var _binding: BottomSheetBinding? = null - private val binding: BottomSheetBinding get() = _binding!! - - interface SessionConfigurationListener { - fun onSessionConfigurationSuccess(sessionUrl: String) - fun onSessionConfigurationDismiss() - } - - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View { - _binding = BottomSheetBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - with (binding) { - sessionUrl.setText(sharedPreferences.getString(SESSION_CONFIGURATION_KEY, "")) - - startSessionButton.setOnClickListener { - sessionConfigurationListener?.onSessionConfigurationSuccess( - sessionUrl.text.toString() - ) - dismiss() - } - } - } - - override fun onDismiss(dialog: DialogInterface) { - sharedPreferences.edit().putString(SESSION_CONFIGURATION_KEY, binding.sessionUrl.text.toString()).apply() - sessionConfigurationListener?.onSessionConfigurationDismiss() - super.onDismiss(dialog) - } - - override fun onAttach(context: Context) { - super.onAttach(context) - sharedPreferences = requireContext().getSharedPreferences(SESSION_PREFERENCES_ID, Context.MODE_PRIVATE) - sessionConfigurationListener = context as? SessionConfigurationListener - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onDetach() { - sessionConfigurationListener = null - super.onDetach() - } - - companion object { - const val FRAGMENT_TAG = "com.yoti.mobile.android.sdk.yotidocscan.websample.SessionBottomSheet" - - fun newInstance() = SessionBottomSheet() - } -} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt new file mode 100644 index 0000000..4bf5146 --- /dev/null +++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/WebScreen.kt @@ -0,0 +1,159 @@ +package com.yoti.mobile.android.sdk.yotidocscan.websample + +import android.annotation.SuppressLint +import android.net.Uri +import android.webkit.PermissionRequest +import android.webkit.ValueCallback +import android.webkit.WebChromeClient +import android.webkit.WebChromeClient.FileChooserParams +import android.webkit.WebSettings +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView + +@Composable +fun WebScreen( + sessionUrl: String, + showSessionFinishedDialog: Boolean, + onPageCommitVisible: (String?) -> Unit, + onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit, + onCloseSession: () -> Unit, + onSessionFinished: () -> Unit, + modifier: Modifier = Modifier +) { + Box(modifier = modifier.fillMaxSize()) { + AndroidWebView(sessionUrl, onPageCommitVisible, onShowCameraAndFilePickerChooser) + + var showCloseSessionDialog by remember { mutableStateOf(false) } + BackHandler { showCloseSessionDialog = true } + if (showCloseSessionDialog) { + CloseSessionDialog( + onConfirm = { + showCloseSessionDialog = false + onCloseSession() + }, + onDismiss = { showCloseSessionDialog = false }) + } + + if (showSessionFinishedDialog) { + SessionFinishedDialog(onConfirm = { onSessionFinished() }) + } + } +} + +@Composable +private fun AndroidWebView( + sessionUrl: String, + onPageCommitVisible: (String?) -> Unit, + onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit +) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + WebView(context).apply { + WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) + configureSettings(settings) + configureWebViewClient(this, onPageCommitVisible) + configureWebChromeClient(this, onShowCameraAndFilePickerChooser) + } + }, + update = { webView -> + sessionUrl.takeIf { it.isNotBlank() }?.let { webView.loadUrl(it) } + } + ) +} + +@Composable +private fun CloseSessionDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) { + AlertDialog( + title = { Text(stringResource(id = R.string.close_session_dialog_title)) }, + text = { Text(stringResource(id = R.string.close_session_dialog_text)) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(id = R.string.close_session_dialog_confirm_button)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(id = R.string.close_session_dialog_dismiss_button)) + } + }, + onDismissRequest = onDismiss + ) +} + +@Composable +private fun SessionFinishedDialog(onConfirm: () -> Unit) { + AlertDialog( + title = { Text(stringResource(id = R.string.session_finished_dialog_title)) }, + text = { Text(stringResource(id = R.string.session_finished_dialog_text)) }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(stringResource(id = R.string.session_finished_dialog_confirm_button)) + } + }, + onDismissRequest = {} + ) +} + +@Suppress("DEPRECATION") +@SuppressLint("SetJavaScriptEnabled") +private fun configureSettings(settings: WebSettings) { + with(settings) { + javaScriptEnabled = true + allowFileAccess = true + allowUniversalAccessFromFileURLs = true + domStorageEnabled = true + javaScriptCanOpenWindowsAutomatically = true + mediaPlaybackRequiresUserGesture = false + } +} + +private fun configureWebViewClient(webView: WebView, onPageCommitVisible: (String?) -> Unit) { + webView.webViewClient = object : WebViewClient() { + override fun onPageCommitVisible(view: WebView?, url: String?) { + super.onPageCommitVisible(view, url) + onPageCommitVisible(url) + } + } +} + +private fun configureWebChromeClient( + webView: WebView, + onShowCameraAndFilePickerChooser: (ValueCallback>?, FileChooserParams) -> Unit +) { + webView.webChromeClient = object : WebChromeClient() { + // Launch file picker or camera intent and set the + // filePathCallback to set the capture results to the webview + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + return if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN) { + onShowCameraAndFilePickerChooser(filePathCallback, fileChooserParams) + true + } else { + false + } + } + + // Grant permission requested + override fun onPermissionRequest(request: PermissionRequest?) { + request?.grant(request.resources) + } + } +} \ No newline at end of file diff --git a/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt new file mode 100644 index 0000000..9b7f132 --- /dev/null +++ b/webapp/src/main/java/com/yoti/mobile/android/sdk/yotidocscan/websample/ui/Theme.kt @@ -0,0 +1,18 @@ +package com.yoti.mobile.android.sdk.yotidocscan.websample.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +private val LightColorScheme = lightColorScheme( + background = Color.White +) + +@Composable +fun YotiDocScanWebSampleAppTheme(content: @Composable () -> Unit) { + MaterialTheme( + colorScheme = LightColorScheme, + content = content + ) +} \ No newline at end of file diff --git a/webapp/src/main/res/drawable/ic_shake.xml b/webapp/src/main/res/drawable/ic_shake.xml deleted file mode 100644 index 1d861ec..0000000 --- a/webapp/src/main/res/drawable/ic_shake.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/webapp/src/main/res/layout/activity_main.xml b/webapp/src/main/res/layout/activity_main.xml deleted file mode 100644 index 3fd8ff7..0000000 --- a/webapp/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/webapp/src/main/res/layout/bottom_sheet.xml b/webapp/src/main/res/layout/bottom_sheet.xml deleted file mode 100644 index 88347fb..0000000 --- a/webapp/src/main/res/layout/bottom_sheet.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - -