diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 626bba40df..3f51404e47 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -16,9 +16,6 @@ android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> - - - bitmap } - return File( - FileResourceDirectoryHelper.getFileResourceDirectory(context), + val file = File( + FileResourceDirectoryHelper.getFileCacheResourceDirectory(context), "tempFile.png", - ).apply { writeBitmap(bitmap, Bitmap.CompressFormat.JPEG, 100) } + ) + + file.writeBitmap(bitmap, Bitmap.CompressFormat.JPEG, 100) + + return file +} + +fun Bitmap.rotateImageAndSave(context: Context): File { + val file = File( + FileResourceDirectoryHelper.getFileCacheResourceDirectory(context), + "tempFile.png", + ) + + file.writeBitmap(this, Bitmap.CompressFormat.JPEG, 100) + + val ei = ExifInterface(file.path) + + val orientation = + ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + + val bitmap = when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(this, 90F) + ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(this, 180F) + ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(this, 270F) + else -> this + } + + file.writeBitmap(bitmap!!, Bitmap.CompressFormat.JPEG, 100) + + return file } private fun rotateImage(source: Bitmap, angle: Float): Bitmap? { @@ -107,9 +136,11 @@ private fun getFilePath(context: Context, uri: Uri): String? { split[splitIndex].toLong(), ) } + isExternalStorageDocument(copy) -> { return Environment.getExternalStorageDirectory().toString() + "/" + split[1] } + isMediaDocument(copy) -> { copy = when (split[0]) { "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI diff --git a/form/src/main/java/org/dhis2/form/model/UiEventType.kt b/form/src/main/java/org/dhis2/form/model/UiEventType.kt index 81eb03f23b..65b1875a7b 100644 --- a/form/src/main/java/org/dhis2/form/model/UiEventType.kt +++ b/form/src/main/java/org/dhis2/form/model/UiEventType.kt @@ -2,8 +2,6 @@ package org.dhis2.form.model enum class UiEventType { REQUEST_LOCATION_BY_MAP, - ADD_PICTURE, - ADD_FILE, OPEN_FILE, SHARE_IMAGE, } diff --git a/form/src/main/java/org/dhis2/form/ui/Form.kt b/form/src/main/java/org/dhis2/form/ui/Form.kt index d030e1d1df..5fc6925e94 100644 --- a/form/src/main/java/org/dhis2/form/ui/Form.kt +++ b/form/src/main/java/org/dhis2/form/ui/Form.kt @@ -147,6 +147,15 @@ fun Form( onNextSection, ) }, + onFileSelected = { path -> + intentHandler.invoke( + FormIntent.OnStoreFile( + uid = fieldUiModel.uid, + filePath = path, + valueType = fieldUiModel.valueType, + ), + ) + }, ) } } diff --git a/form/src/main/java/org/dhis2/form/ui/FormView.kt b/form/src/main/java/org/dhis2/form/ui/FormView.kt index f3f4b34681..6f68c2946e 100644 --- a/form/src/main/java/org/dhis2/form/ui/FormView.kt +++ b/form/src/main/java/org/dhis2/form/ui/FormView.kt @@ -3,9 +3,7 @@ package org.dhis2.form.ui import android.Manifest import android.app.Activity.RESULT_OK import android.content.ActivityNotFoundException -import android.content.DialogInterface import android.content.Intent -import android.content.pm.PackageManager import android.net.Uri import android.os.Build import android.os.Bundle @@ -15,7 +13,6 @@ import android.view.View import android.view.ViewGroup import android.widget.Toast import androidx.activity.result.contract.ActivityResultContracts -import androidx.annotation.RequiresApi import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState @@ -28,12 +25,7 @@ import androidx.fragment.app.viewModels import androidx.paging.compose.collectAsLazyPagingItems import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.journeyapps.barcodescanner.ScanOptions -import org.dhis2.commons.ActivityResultObservable -import org.dhis2.commons.ActivityResultObserver import org.dhis2.commons.Constants -import org.dhis2.commons.bindings.getFileFrom -import org.dhis2.commons.bindings.getFileFromGallery -import org.dhis2.commons.bindings.rotateImage import org.dhis2.commons.data.FileHandler import org.dhis2.commons.data.FormFileProvider import org.dhis2.commons.date.DateUtils @@ -77,7 +69,6 @@ import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialog import org.dhis2.ui.dialogs.bottomsheet.BottomSheetDialogUiModel import org.dhis2.ui.dialogs.bottomsheet.DialogButtonStyle import org.dhis2.ui.dialogs.bottomsheet.FieldWithIssue -import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper import org.hisp.dhis.android.core.common.ValueType import org.hisp.dhis.android.core.common.ValueTypeRenderingType import org.hisp.dhis.android.core.event.EventStatus @@ -131,91 +122,6 @@ class FormView : Fragment() { } } - private val requestCameraPermissions = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { - if (it.values.all { isGranted -> isGranted }) { - showAddImageOptions() - (context as ActivityResultObservable?)?.subscribe(object : ActivityResultObserver { - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent?, - ) { - if (resultCode != RESULT_OK) { - showAddImageOptions() - } - } - - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray, - ) { - if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { - showAddImageOptions() - } - } - }) - } else { - Toast.makeText( - requireContext(), - requireContext().getString(R.string.camera_permission_denied), - Toast.LENGTH_LONG, - ).show() - } - } - - private val takePicture = - registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> - if (success) { - val imageFile = File( - FileResourceDirectoryHelper.getFileResourceDirectory(requireContext()), - TEMP_FILE, - ).rotateImage(requireContext()) - onSavePicture?.invoke(imageFile.path) - - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } else { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } - } - - private val pickImage = - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { activityResult -> - if (activityResult.resultCode == RESULT_OK) { - getFileFromGallery(requireContext(), activityResult.data?.data)?.also { file -> - onSavePicture?.invoke(file.path) - } - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } else { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } - } - - private val pickFile = - registerForActivityResult(ActivityResultContracts.GetContent()) { uri -> - if (uri != null) { - getFileFrom(requireContext(), uri)?.also { file -> - onSavePicture?.invoke(file.path) - } - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } else { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } - } - private val requestStoragePermissions = registerForActivityResult( ActivityResultContracts.RequestPermission(), @@ -245,21 +151,6 @@ class FormView : Fragment() { private lateinit var formSectionMapper: FormSectionMapper var scrollCallback: ((Boolean) -> Unit)? = null private var displayConfErrors = true - private var onSavePicture: ((String) -> Unit)? = null - - private val storagePermissions = arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE, - ) - - @RequiresApi(api = Build.VERSION_CODES.TIRAMISU) - private val storagePermissions33 = arrayOf( - Manifest.permission.CAMERA, - Manifest.permission.READ_MEDIA_IMAGES, - Manifest.permission.READ_MEDIA_AUDIO, - Manifest.permission.READ_MEDIA_VIDEO, - ) private val fileHandler = FileHandler() @@ -553,9 +444,7 @@ class FormView : Fragment() { is RecyclerViewUiEvents.DisplayQRCode -> displayQRImage(uiEvent) is RecyclerViewUiEvents.ScanQRCode -> requestQRScan(uiEvent) is RecyclerViewUiEvents.OpenOrgUnitDialog -> showOrgUnitDialog(uiEvent) - is RecyclerViewUiEvents.AddImage -> requestAddImage(uiEvent) is RecyclerViewUiEvents.OpenFile -> openFile(uiEvent) - is RecyclerViewUiEvents.OpenFileSelector -> openFileSelector(uiEvent) is RecyclerViewUiEvents.OpenChooserIntent -> openChooserIntent(uiEvent) is RecyclerViewUiEvents.SelectPeriod -> showPeriodDialog(uiEvent) } @@ -699,84 +588,6 @@ class FormView : Fragment() { ) } - private fun requestAddImage(event: RecyclerViewUiEvents.AddImage) { - onSavePicture = { picture -> - intentHandler( - FormIntent.OnStoreFile( - event.uid, - picture, - ValueType.IMAGE, - ), - ) - } - requestCameraPermissions.launch(permissions()) - } - - private fun permissions() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - storagePermissions33 - } else { - storagePermissions - } - - private fun showAddImageOptions() { - val options = arrayOf( - requireContext().getString(R.string.take_photo), - requireContext().getString(R.string.from_gallery), - requireContext().getString(R.string.cancel), - ) - MaterialAlertDialogBuilder(requireActivity(), R.style.MaterialDialog) - .setTitle(requireContext().getString(R.string.select_option)) - .setOnCancelListener { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } - .setItems(options) { dialog: DialogInterface, item: Int -> - run { - when (options[item]) { - requireContext().getString(R.string.take_photo) -> { - val photoUri = FileProvider.getUriForFile( - requireContext(), - FormFileProvider.fileProviderAuthority, - File( - FileResourceDirectoryHelper.getFileResourceDirectory( - requireContext(), - ), - TEMP_FILE, - ), - ) - takePicture.launch(photoUri) - } - - requireContext().getString(R.string.from_gallery) -> { - pickImage.launch(Intent(Intent.ACTION_PICK).apply { type = "image/*" }) - } - - requireContext().getString(R.string.cancel) -> { - viewModel.getFocusedItemUid()?.let { - viewModel.submitIntent(FormIntent.OnAddImageFinished(it)) - } - } - } - dialog.dismiss() - } - } - .show() - } - - private fun openFileSelector(event: RecyclerViewUiEvents.OpenFileSelector) { - onSavePicture = { file -> - intentHandler( - FormIntent.OnStoreFile( - event.field.uid, - file, - event.field.valueType, - ), - ) - } - pickFile.launch("*/*") - } - private fun openFile(event: RecyclerViewUiEvents.OpenFile) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) { downloadFile(event.field.displayName) diff --git a/form/src/main/java/org/dhis2/form/ui/dialog/ImagePickerOptionsDialog.kt b/form/src/main/java/org/dhis2/form/ui/dialog/ImagePickerOptionsDialog.kt new file mode 100644 index 0000000000..d376ea44cd --- /dev/null +++ b/form/src/main/java/org/dhis2/form/ui/dialog/ImagePickerOptionsDialog.kt @@ -0,0 +1,83 @@ +package org.dhis2.form.ui.dialog + +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.CameraAlt +import androidx.compose.material.icons.outlined.Collections +import androidx.compose.material.icons.outlined.Image +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import org.dhis2.form.R +import org.hisp.dhis.mobile.ui.designsystem.component.BottomSheetShell +import org.hisp.dhis.mobile.ui.designsystem.component.ButtonCarousel +import org.hisp.dhis.mobile.ui.designsystem.component.CarouselButtonData +import org.hisp.dhis.mobile.ui.designsystem.theme.SurfaceColor +import org.hisp.dhis.mobile.ui.designsystem.theme.TextColor + +@Composable +internal fun ImagePickerOptionsDialog( + title: String, + showImageOptions: Boolean, + onDismiss: () -> Unit, + onTakePicture: (Context) -> Unit, + onSelectFromGallery: () -> Unit, +) { + AnimatedVisibility( + visible = showImageOptions, + enter = slideInVertically() + fadeIn(), + exit = slideOutVertically() + fadeOut(), + ) { + BottomSheetShell( + title = title, + icon = { + Icon(Icons.Outlined.Image, contentDescription = null, tint = SurfaceColor.Primary) + }, + onDismiss = onDismiss, + buttonBlock = { + val context = LocalContext.current + ButtonCarousel( + carouselButtonList = listOf( + CarouselButtonData( + onClick = { + onDismiss() + onTakePicture(context) + }, + enabled = true, + text = stringResource(R.string.take_photo), + icon = { + Icon( + Icons.Outlined.CameraAlt, + contentDescription = null, + tint = TextColor.OnSurface, + ) + }, + ), + CarouselButtonData( + onClick = { + onDismiss() + onSelectFromGallery() + }, + enabled = true, + text = stringResource(R.string.from_gallery_v2), + icon = { + Icon( + Icons.Outlined.Collections, + contentDescription = null, + tint = TextColor.OnSurface, + ) + }, + ), + ), + ) + }, + content = null, + ) + } +} diff --git a/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt b/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt index f28f0d830e..eaa7eefeeb 100644 --- a/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt +++ b/form/src/main/java/org/dhis2/form/ui/event/RecyclerViewUiEvents.kt @@ -37,14 +37,6 @@ sealed class RecyclerViewUiEvents { val orgUnitSelectorScope: OrgUnitSelectorScope?, ) : RecyclerViewUiEvents() - data class AddImage( - val uid: String, - ) : RecyclerViewUiEvents() - - data class OpenFileSelector( - val field: FieldUiModel, - ) : RecyclerViewUiEvents() - data class OpenFile( val field: FieldUiModel, ) : RecyclerViewUiEvents() diff --git a/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt b/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt index f05f27c5aa..3b8c7eb56d 100644 --- a/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt +++ b/form/src/main/java/org/dhis2/form/ui/event/UiEventFactoryImpl.kt @@ -3,8 +3,6 @@ package org.dhis2.form.ui.event import android.content.Intent import org.dhis2.form.model.FieldUiModel import org.dhis2.form.model.UiEventType -import org.dhis2.form.model.UiEventType.ADD_FILE -import org.dhis2.form.model.UiEventType.ADD_PICTURE import org.dhis2.form.model.UiEventType.OPEN_FILE import org.dhis2.form.model.UiEventType.REQUEST_LOCATION_BY_MAP import org.dhis2.form.model.UiEventType.SHARE_IMAGE @@ -35,9 +33,6 @@ class UiEventFactoryImpl( featureType = getFeatureType(renderingType), value = value, ) - - ADD_PICTURE -> RecyclerViewUiEvents.AddImage(uid) - ADD_FILE -> RecyclerViewUiEvents.OpenFileSelector(fieldUiModel) OPEN_FILE -> RecyclerViewUiEvents.OpenFile(fieldUiModel) SHARE_IMAGE -> RecyclerViewUiEvents.OpenChooserIntent( Intent.ACTION_SEND, diff --git a/form/src/main/java/org/dhis2/form/ui/files/GetFileResource.kt b/form/src/main/java/org/dhis2/form/ui/files/GetFileResource.kt new file mode 100644 index 0000000000..03c6b3b375 --- /dev/null +++ b/form/src/main/java/org/dhis2/form/ui/files/GetFileResource.kt @@ -0,0 +1,134 @@ +package org.dhis2.form.ui.files + +import android.app.Activity +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.webkit.MimeTypeMap +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.FileProvider +import org.dhis2.commons.bindings.rotateImage +import org.dhis2.commons.data.FormFileProvider +import org.hisp.dhis.android.core.arch.helpers.FileResourceDirectoryHelper +import java.io.File + +private class GetFileResource( + private val allowMultipleSelection: Boolean = false, +) : ActivityResultContract>() { + override fun createIntent(context: Context, input: String): Intent { + return Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = input + putExtra(Intent.EXTRA_LOCAL_ONLY, true) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultipleSelection) + .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): List { + return intent.takeIf { + resultCode == Activity.RESULT_OK + }?.getClipDataUris() ?: emptyList() + } +} + +@Composable +fun rememberFilePicker( + onResult: (String) -> Unit, +) = with(LocalContext.current) { + val launcher = rememberLauncherForActivityResult( + contract = GetFileResource(), + onResult = { uris -> + uris.firstOrNull()?.toFile(context = this)?.path?.let(onResult) + }, + ) + return@with launcher +} + +@Composable +fun rememberCameraPicker( + onSuccess: (String) -> Unit, + onError: () -> Unit, + onPermissionAccepted: () -> Unit, +) = with( + LocalContext.current, +) { + val tempFile = File( + FileResourceDirectoryHelper.getFileResourceDirectory(this), + "tempFile.png", + ) + + val photoUri = FileProvider.getUriForFile( + this, + FormFileProvider.fileProviderAuthority, + tempFile, + ) + + val launcher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture(), + onResult = { success -> + if (success) { + onSuccess(tempFile.rotateImage(this).path) + } else { + onError() + } + }, + ) + val cameraPermission = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission(), + ) { accepted -> + if (accepted) { + onPermissionAccepted() + launcher.launch(photoUri) + } else { + onError() + } + } + + return@with Triple(photoUri, launcher, cameraPermission) +} + +internal fun Uri.toFile(context: Context, suffix: String = ""): File? { + var resultFile: File? = null + if (ContentResolver.SCHEME_CONTENT == this.scheme) { + val cr = context.contentResolver + val mimeTypeMap = MimeTypeMap.getSingleton() + val extensionsFile = mimeTypeMap.getExtensionFromMimeType(cr.getType(this)) + resultFile = File.createTempFile( + "tempFile", + "$suffix.$extensionsFile", + context.cacheDir, + ) + val input = cr.openInputStream(this) + resultFile.outputStream().use { stream -> + input?.copyTo(stream) + } + input?.close() + } + return resultFile +} + +internal fun Intent.getClipDataUris(): List { + val resultSet = LinkedHashSet() + data?.let { data -> + resultSet.add(data) + } + val clipData = clipData + if (clipData == null && resultSet.isEmpty()) { + return emptyList() + } else if (clipData != null) { + for (i in 0 until clipData.itemCount) { + val uri = clipData.getItemAt(i).uri + if (uri != null) { + resultSet.add(uri) + } + } + } + return ArrayList(resultSet) +} diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt index 2515ddf353..e709a498f6 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/FieldProvider.kt @@ -65,6 +65,7 @@ fun FieldProvider( resources: ResourceManager, focusManager: FocusManager, onNextClicked: () -> Unit, + onFileSelected: (String) -> Unit, ) { val bringIntoViewRequester = remember { BringIntoViewRequester() } val focusRequester = remember { FocusRequester() } @@ -136,6 +137,7 @@ fun FieldProvider( focusRequester = focusRequester, onNextClicked = onNextClicked, focusManager = focusManager, + onFileSelected = onFileSelected, ) } } @@ -151,6 +153,7 @@ fun ProvideByValueType( focusRequester: FocusRequester, onNextClicked: () -> Unit, focusManager: FocusManager, + onFileSelected: (String) -> Unit, ) { when (fieldUiModel.valueType) { ValueType.TEXT -> { @@ -298,6 +301,7 @@ fun ProvideByValueType( modifier = modifier, fieldUiModel = fieldUiModel, resources = resources, + onFileSelected = onFileSelected, uiEventHandler = uiEventHandler, ) } @@ -419,6 +423,7 @@ fun ProvideByValueType( intentHandler = intentHandler, uiEventHandler = uiEventHandler, resources = resources, + onFileSelected = onFileSelected, ) } } @@ -743,7 +748,12 @@ private fun ProvideIntegerNegative( ) } var value by remember(fieldUiModel.value) { - mutableStateOf(TextFieldValue(fieldUiModel.value?.replace("-", "") ?: "", savedTextSelection)) + mutableStateOf( + TextFieldValue( + fieldUiModel.value?.replace("-", "") ?: "", + savedTextSelection, + ), + ) } InputNegativeInteger( diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt index e1a0e5d67e..a6014a5968 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/ImageInputProvider.kt @@ -1,6 +1,8 @@ package org.dhis2.form.ui.provider.inputfield +import android.Manifest import android.content.Intent +import android.content.pm.PackageManager import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -10,6 +12,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.core.content.ContextCompat import org.dhis2.commons.extensions.getBitmap import org.dhis2.commons.resources.ResourceManager import org.dhis2.form.R @@ -17,8 +20,10 @@ import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText import org.dhis2.form.model.FieldUiModel -import org.dhis2.form.model.UiEventType +import org.dhis2.form.ui.dialog.ImagePickerOptionsDialog import org.dhis2.form.ui.event.RecyclerViewUiEvents +import org.dhis2.form.ui.files.rememberCameraPicker +import org.dhis2.form.ui.files.rememberFilePicker import org.dhis2.form.ui.intent.FormIntent import org.hisp.dhis.mobile.ui.designsystem.component.InputImage import org.hisp.dhis.mobile.ui.designsystem.component.UploadState @@ -30,10 +35,39 @@ internal fun ProvideInputImage( resources: ResourceManager, intentHandler: (FormIntent) -> Unit, uiEventHandler: (RecyclerViewUiEvents) -> Unit, + onFileSelected: (filePath: String) -> Unit, ) { - var uploadState by remember(fieldUiModel) { mutableStateOf(getUploadState(fieldUiModel.displayName, fieldUiModel.isLoadingData)) } + var showImageOptions by remember { mutableStateOf(false) } + + var uploadState by remember(fieldUiModel) { + mutableStateOf( + getUploadState( + fieldUiModel.displayName, + fieldUiModel.isLoadingData, + ), + ) + } val painter = fieldUiModel.displayName?.getBitmap()?.let { BitmapPainter(it.asImageBitmap()) } + + val filePicker = rememberFilePicker(onFileSelected) + + val (tempFileUri, imagePicker, cameraPermission) = rememberCameraPicker( + onSuccess = { filePath -> + onFileSelected(filePath) + uploadState = getUploadState(fieldUiModel.displayName, false) + }, + onError = { + uploadState = getUploadState(fieldUiModel.displayName, false) + intentHandler.invoke( + FormIntent.OnAddImageFinished(fieldUiModel.uid), + ) + }, + onPermissionAccepted = { + uploadState = getUploadState(fieldUiModel.displayName, true) + }, + ) + InputImage( modifier = modifier.fillMaxWidth(), title = fieldUiModel.label, @@ -69,8 +103,28 @@ internal fun ProvideInputImage( ) }, onAddButtonClicked = { - uploadState = getUploadState(fieldUiModel.displayName, true) - fieldUiModel.invokeUiEvent(UiEventType.ADD_PICTURE) + showImageOptions = true + }, + ) + + ImagePickerOptionsDialog( + title = fieldUiModel.label, + showImageOptions = showImageOptions, + onDismiss = { showImageOptions = false }, + onTakePicture = { context -> + if (ContextCompat.checkSelfPermission( + context, + Manifest.permission.CAMERA, + ) == PackageManager.PERMISSION_GRANTED + ) { + uploadState = getUploadState(fieldUiModel.displayName, true) + imagePicker.launch(tempFileUri) + } else { + cameraPermission.launch(Manifest.permission.CAMERA) + } + }, + onSelectFromGallery = { + filePicker.launch("image/*") }, ) } diff --git a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt index f6f03c16c3..a56369475c 100644 --- a/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt +++ b/form/src/main/java/org/dhis2/form/ui/provider/inputfield/InputFileProvider.kt @@ -13,8 +13,8 @@ import org.dhis2.form.extensions.inputState import org.dhis2.form.extensions.legend import org.dhis2.form.extensions.supportingText import org.dhis2.form.model.FieldUiModel -import org.dhis2.form.model.UiEventType import org.dhis2.form.ui.event.RecyclerViewUiEvents +import org.dhis2.form.ui.files.rememberFilePicker import org.hisp.dhis.mobile.ui.designsystem.component.InputFileResource import org.hisp.dhis.mobile.ui.designsystem.component.UploadFileState import java.io.File @@ -25,6 +25,7 @@ internal fun ProvideInputFileResource( modifier: Modifier, fieldUiModel: FieldUiModel, resources: ResourceManager, + onFileSelected: (filePath: String) -> Unit, uiEventHandler: (RecyclerViewUiEvents) -> Unit, ) { var uploadState by remember(fieldUiModel) { @@ -37,6 +38,8 @@ internal fun ProvideInputFileResource( } val file = fieldUiModel.displayName?.let { File(it) } + val filePicker = rememberFilePicker(onFileSelected) + InputFileResource( modifier = modifier.fillMaxWidth(), title = fieldUiModel.label, @@ -48,7 +51,7 @@ internal fun ProvideInputFileResource( fileWeight = file?.length()?.let { fileSizeLabel(it) }, onSelectFile = { uploadState = getFileUploadState(fieldUiModel.displayName, true) - fieldUiModel.invokeUiEvent(UiEventType.ADD_FILE) + filePicker.launch("*/*") }, onClear = { fieldUiModel.onClear() }, onUploadFile = { diff --git a/form/src/main/res/values/strings.xml b/form/src/main/res/values/strings.xml index c64d2249ac..28d8b9d85f 100644 --- a/form/src/main/res/values/strings.xml +++ b/form/src/main/res/values/strings.xml @@ -78,7 +78,7 @@ Copied No info for this field Take Photo - Choose from gallery + From gallery Check this! Do not show again the provided value is not allowed for this field