From a1af5880c516687d227b939e6ff5acefe2c8539d Mon Sep 17 00:00:00 2001 From: Andrey T Date: Thu, 21 Jul 2022 14:55:18 +0200 Subject: [PATCH 01/20] kmp sample android integration --- README.md | 27 ++- androidApp/src/main/AndroidManifest.xml | 25 +++ .../kmp/example/android/BallerineKYCFlow.kt | 177 ++++++++++++++++++ .../kmp/example/android/CameraFileProvider.kt | 5 + .../kmp/example/android/MainActivity.kt | 34 +++- .../kmp/example/android/utils/FileUtil.kt | 50 +++++ .../example/android/utils/PermissionUtil.kt | 45 +++++ .../src/main/res/layout/activity_main.xml | 26 ++- .../activity_user_registration_flow.xml | 14 ++ .../res/xml/image_picker_provider_paths.xml | 12 ++ build.gradle.kts | 1 + .../io/ballerine/kmp/example/Platform.kt | 1 + .../ballerine/kmp/example/BallerineStorage.kt | 18 ++ 13 files changed, 419 insertions(+), 16 deletions(-) create mode 100644 androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt create mode 100644 androidApp/src/main/java/io/ballerine/kmp/example/android/CameraFileProvider.kt create mode 100644 androidApp/src/main/java/io/ballerine/kmp/example/android/utils/FileUtil.kt create mode 100644 androidApp/src/main/java/io/ballerine/kmp/example/android/utils/PermissionUtil.kt create mode 100644 androidApp/src/main/res/layout/activity_user_registration_flow.xml create mode 100644 androidApp/src/main/res/xml/image_picker_provider_paths.xml create mode 100644 shared/src/commonMain/kotlin/io/ballerine/kmp/example/BallerineStorage.kt diff --git a/README.md b/README.md index 71109ed..6bda10b 100644 --- a/README.md +++ b/README.md @@ -1 +1,26 @@ -# ballerine-kmp-example \ No newline at end of file +## Ballerine Integration example + +### Integration into Android version of KMP project + +1. Create Fragment or Activity which contains WebView +2. Set webViewSettings the following WebView settings +```kt + webviewSettings.javaScriptEnabled = true + webviewSettings.domStorageEnabled = true + webviewSettings.allowFileAccess = true +``` +3. Setup WebViewClient and WebChromeClient. In WebChromeClient override method `onShowFileChooser` the same way as it is implemented in `BallerineKYCFlow`. +4. For taking photo create a camera image file and pass it to camera intent. +```kt + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) +``` +5. After receiving callback from Camera app, pass Uri of the camera image file to `filePathCallback`, as in example: +```kt + // Use Uri object instead of File to avoid storage permissions + filePathCallback?.onReceiveValue(arrayOf(uri)) + filePathCallback = null + ... +} +``` +5. Create method that checking about finished state of the registration flow and save received results, see `checkWebViewUrl` method for more details. \ No newline at end of file diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 716387d..058260c 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -1,6 +1,14 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt b/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt new file mode 100644 index 0000000..bae0661 --- /dev/null +++ b/androidApp/src/main/java/io/ballerine/kmp/example/android/BallerineKYCFlow.kt @@ -0,0 +1,177 @@ +package io.ballerine.kmp.example.android + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.* +import android.provider.MediaStore +import android.util.Log +import android.webkit.* +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider +import io.ballerine.kmp.example.BallerineStorage +import io.ballerine.kmp.example.android.utils.FileUtil +import io.ballerine.kmp.example.android.utils.PermissionUtil +import java.io.File + + +class BallerineKYCFlow : AppCompatActivity() { + + lateinit var webView: WebView + companion object{ + private val REQUIRED_PERMISSIONS = arrayOf( + Manifest.permission.CAMERA + ) + private const val CAMERA_INTENT_REQ_CODE = 4281 + private const val PERMISSION_INTENT_REQ_CODE = 4282 + } + private var filePathCallback: ValueCallback>? = null + private var mCameraFile: File? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_user_registration_flow) + + webView = findViewById(R.id.web_view) + + webView.webViewClient = object : WebViewClient() {} + + setWebViewSettings(webView.settings) + webView.webChromeClient = object : WebChromeClient() { + + override fun onShowFileChooser( + webView: WebView?, + filePathCallback: ValueCallback>?, + fileChooserParams: FileChooserParams? + ): Boolean { + Log.d("TAG", "Permission Request") + if (this@BallerineKYCFlow.filePathCallback != null) { + this@BallerineKYCFlow.filePathCallback!!.onReceiveValue(null) + } + this@BallerineKYCFlow.filePathCallback = filePathCallback + checkPermission() + return true + } + } + + loadUrl() + checkWebViewUrl() + } + + + + private fun checkWebViewUrl(){ + Handler(Looper.getMainLooper()).postDelayed({ + if(webView.url?.contains("final") == true){ + val uri = Uri.parse(webView.url) + BallerineStorage.saveSecret(uri.query ?: "") + setResult(Activity.RESULT_OK) + finish() + return@postDelayed + } + checkWebViewUrl() + }, 2000) + } + + private fun setWebViewSettings(webviewSettings: WebSettings) { + webviewSettings.javaScriptEnabled = true + webviewSettings.domStorageEnabled = true + webviewSettings.allowFileAccess = true + } + + + private fun loadUrl() { + webView.loadUrl("https://2.dev.ballerine.app") + } + + + private fun checkPermission() { + if (isPermissionGranted(this)) { + // Permission Granted, Start Camera Intent + startCameraIntent() + } else { + // Request Permission + requestPermission() + } + } + + private fun startCameraIntent() { + // Create and get empty file to store capture image content + val file = FileUtil.getImageFile(fileDir = getExternalFilesDir(Environment.DIRECTORY_DCIM) ?: filesDir) + mCameraFile = file + + // Check if file exists + if (file != null && file.exists()) { + val cameraIntent = getCameraIntent(this, file) + startActivityForResult(cameraIntent, CAMERA_INTENT_REQ_CODE) + } + } + + private fun getCameraIntent(context: Context, file: File): Intent? { + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + val authority = + context.packageName + ".imagepicker.provider" + val photoURI = FileProvider.getUriForFile(context, authority, file) + intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI) + } else { + intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file)) + } + + return intent + } + + private fun requestPermission() { + ActivityCompat.requestPermissions( + this, + getRequiredPermission(this), + PERMISSION_INTENT_REQ_CODE + ) + } + + private fun isPermissionGranted(context: Context): Boolean { + return getRequiredPermission(context).none { + !PermissionUtil.isPermissionGranted(context, it) + } + } + + private fun getRequiredPermission(context: Context): Array { + return REQUIRED_PERMISSIONS.filter { + PermissionUtil.isPermissionInManifest(context, it) + }.toTypedArray() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PERMISSION_INTENT_REQ_CODE) { + // Check again if permission is granted + if (isPermissionGranted(this)) { + // Permission is granted, Start Camera Intent + startCameraIntent() + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode == CAMERA_INTENT_REQ_CODE) { + if (resultCode == Activity.RESULT_OK) { + filePathCallback?.onReceiveValue(arrayOf(Uri.fromFile(mCameraFile))) + filePathCallback = null + } else { + Toast.makeText(this, "Task Cancelled", Toast.LENGTH_SHORT).show() + } + } + } + + +} diff --git a/androidApp/src/main/java/io/ballerine/kmp/example/android/CameraFileProvider.kt b/androidApp/src/main/java/io/ballerine/kmp/example/android/CameraFileProvider.kt new file mode 100644 index 0000000..7129e89 --- /dev/null +++ b/androidApp/src/main/java/io/ballerine/kmp/example/android/CameraFileProvider.kt @@ -0,0 +1,5 @@ +package io.ballerine.kmp.example.android + +import androidx.core.content.FileProvider + +class CameraFileProvider : FileProvider() diff --git a/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt b/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt index a81cfd9..cc5a28e 100644 --- a/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt +++ b/androidApp/src/main/java/io/ballerine/kmp/example/android/MainActivity.kt @@ -1,20 +1,40 @@ package io.ballerine.kmp.example.android +import android.app.Activity +import android.content.Intent import androidx.appcompat.app.AppCompatActivity import android.os.Bundle -import io.ballerine.kmp.example.Greeting +import android.widget.Button import android.widget.TextView - -fun greet(): String { - return Greeting().greeting() -} +import androidx.activity.result.contract.ActivityResultContracts +import io.ballerine.kmp.example.BallerineStorage class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val button = findViewById