diff --git a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.kt b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.kt index 7b888921fab..6e0f122280b 100644 --- a/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.kt +++ b/collect_app/src/androidTest/java/org/odk/collect/android/feature/formentry/IntentGroupTest.kt @@ -299,8 +299,9 @@ class IntentGroupTest { } private fun assertFileWidgetWithoutAnswer() { - onView(withTagValue(Matchers.`is`("ArbitraryFileWidgetAnswer"))) - .check(matches(CoreMatchers.not(isDisplayed()))) + composeRule + .onNodeWithClickLabel(ApplicationProvider.getApplicationContext().getString(R.string.open_file)) + .assertDoesNotExist() } private fun assertImageWidgetWithAnswer() { @@ -329,9 +330,9 @@ class IntentGroupTest { } private fun assertFileWidgetWithAnswer() { - onView(withTagValue(Matchers.`is`("ArbitraryFileWidgetAnswer"))) - .perform(scrollTo()) - .check(matches(isDisplayed())) + composeRule + .onNodeWithClickLabel(ApplicationProvider.getApplicationContext().getString(R.string.open_file)) + .assertExists() } @Throws(IOException::class) diff --git a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java index 3bb7ebd9a59..d5d7652c0d3 100644 --- a/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java +++ b/collect_app/src/main/java/org/odk/collect/android/formentry/ODKView.java @@ -188,8 +188,7 @@ public ODKView( new FileRequesterImpl(intentLauncher, externalAppIntentProvider, formController), new StringRequesterImpl(intentLauncher, externalAppIntentProvider, formController), formController, - (FormFillingActivity) context, - settingsProvider + (FormFillingActivity) context ); widgets = new ArrayList<>(); diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidget.kt deleted file mode 100644 index 6c320712b41..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidget.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.odk.collect.android.widgets - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.view.View -import org.javarosa.form.api.FormEntryPrompt -import org.odk.collect.android.databinding.ArbitraryFileWidgetBinding -import org.odk.collect.android.formentry.questions.QuestionDetails -import org.odk.collect.android.utilities.ApplicationConstants -import org.odk.collect.android.utilities.QuestionMediaManager -import org.odk.collect.android.widgets.interfaces.FileWidget -import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver -import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry - -@SuppressLint("ViewConstructor") -class ArbitraryFileWidget( - context: Context, - questionDetails: QuestionDetails, - private val widgetAnswerView: WidgetAnswerView, - questionMediaManager: QuestionMediaManager, - waitingForDataRegistry: WaitingForDataRegistry, - dependencies: Dependencies -) : BaseArbitraryFileWidget( - context, - questionDetails, - questionMediaManager, - waitingForDataRegistry, - dependencies - ), FileWidget, WidgetDataReceiver { - lateinit var binding: ArbitraryFileWidgetBinding - - init { - render() - } - - override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { - binding = ArbitraryFileWidgetBinding.inflate((context as Activity).layoutInflater) - setupAnswerFile(prompt.answerText) - - binding.arbitraryFileButton.visibility = if (questionDetails.isReadOnly) GONE else VISIBLE - binding.arbitraryFileButton.setOnClickListener { onButtonClick() } - binding.answerViewContainer.setOnClickListener { - mediaUtils.openFile( - getContext(), - answerFile!!, - null - ) - } - if (answerFile != null) { - widgetAnswerView.setAnswer(answerFile!!.name) - binding.answerViewContainer.visibility = VISIBLE - } else { - binding.answerViewContainer.visibility = GONE - } - binding.answerViewContainer.addView(widgetAnswerView) - - return binding.root - } - - override fun clearAnswer() { - binding.answerViewContainer.visibility = GONE - deleteFile() - widgetValueChanged() - } - - private fun onButtonClick() { - waitingForDataRegistry.waitForData(formEntryPrompt.index) - mediaUtils.pickFile( - (context as Activity), - "*/*", - ApplicationConstants.RequestCodes.ARBITRARY_FILE_CHOOSER - ) - } - - override fun setOnLongClickListener(listener: OnLongClickListener?) { - binding.arbitraryFileButton.setOnLongClickListener(listener) - binding.answerViewContainer.setOnLongClickListener(listener) - } - - override fun showAnswerText() { - widgetAnswerView.setAnswer(answerFile!!.name) - binding.answerViewContainer.visibility = VISIBLE - } - - override fun hideAnswerText() { - binding.answerViewContainer.visibility = GONE - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidgetAnswerView.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidgetAnswerView.kt deleted file mode 100644 index a3056f1afeb..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ArbitraryFileWidgetAnswerView.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.odk.collect.android.widgets - -import android.content.Context -import android.util.TypedValue -import android.view.LayoutInflater -import org.odk.collect.android.databinding.ArbitraryFileWidgetAnswerViewBinding - -class ArbitraryFileWidgetAnswerView( - context: Context, - private val fontSize: Int -) : WidgetAnswerView(context) { - private val binding = ArbitraryFileWidgetAnswerViewBinding.inflate(LayoutInflater.from(context), this, true) - - init { - setFontSize() - } - - override fun setAnswer(answer: String?) { - binding.answer.text = answer - } - - override fun setFontSize() { - binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize.toFloat()) - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswerView.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswerView.kt deleted file mode 100644 index f3a3ffdad64..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswerView.kt +++ /dev/null @@ -1,25 +0,0 @@ -package org.odk.collect.android.widgets - -import android.content.Context -import android.util.TypedValue -import android.view.LayoutInflater -import org.odk.collect.android.databinding.BarcodeWidgetAnswerViewBinding - -class BarcodeWidgetAnswerView( - context: Context, - private val fontSize: Int -) : WidgetAnswerView(context) { - private val binding = BarcodeWidgetAnswerViewBinding.inflate(LayoutInflater.from(context), this, true) - - init { - setFontSize() - } - - override fun setAnswer(answer: String?) { - binding.answer.text = answer - } - - override fun setFontSize() { - binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize.toFloat()) - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BaseArbitraryFileWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/BaseArbitraryFileWidget.kt deleted file mode 100644 index c0edac849a9..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/BaseArbitraryFileWidget.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.odk.collect.android.widgets - -import android.content.Context -import org.javarosa.core.model.data.IAnswerData -import org.javarosa.core.model.data.StringData -import org.odk.collect.android.formentry.questions.QuestionDetails -import org.odk.collect.android.utilities.QuestionMediaManager -import org.odk.collect.android.widgets.interfaces.FileWidget -import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver -import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry -import timber.log.Timber -import java.io.File - -abstract class BaseArbitraryFileWidget( - context: Context, - questionDetails: QuestionDetails, - private val questionMediaManager: QuestionMediaManager, - protected val waitingForDataRegistry: WaitingForDataRegistry, - dependencies: Dependencies -) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver { - var answerFile: File? = null - - override fun getAnswer(): IAnswerData? { - return if (answerFile != null) StringData(answerFile!!.name) else null - } - - override fun deleteFile() { - questionMediaManager.deleteAnswerFile( - formEntryPrompt.index.toString(), - answerFile!!.absolutePath - ) - answerFile = null - hideAnswerText() - } - - override fun setData(answer: Any) { - if (answerFile != null) { - deleteFile() - } - - if (answer is File) { - answerFile = answer - if (answerFile!!.exists()) { - questionMediaManager.replaceAnswerFile( - formEntryPrompt.index.toString(), - answerFile!!.absolutePath - ) - showAnswerText() - widgetValueChanged() - } else { - answerFile = null - Timber.e(Error("Inserting Arbitrary file FAILED")) - } - } else if (answer != null) { - Timber.e(Error("FileWidget's setBinaryData must receive a File but received: " + answer.javaClass)) - } - } - - protected fun setupAnswerFile(fileName: String?) { - if (!fileName.isNullOrEmpty()) { - answerFile = questionMediaManager.getAnswerFile(fileName) - } - } - - protected abstract fun showAnswerText() - - protected abstract fun hideAnswerText() -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/ExArbitraryFileWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/ExArbitraryFileWidget.kt deleted file mode 100644 index fce25644170..00000000000 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/ExArbitraryFileWidget.kt +++ /dev/null @@ -1,91 +0,0 @@ -package org.odk.collect.android.widgets - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context -import android.view.View -import org.javarosa.form.api.FormEntryPrompt -import org.odk.collect.android.databinding.ExArbitraryFileWidgetBinding -import org.odk.collect.android.formentry.questions.QuestionDetails -import org.odk.collect.android.utilities.ApplicationConstants -import org.odk.collect.android.utilities.QuestionMediaManager -import org.odk.collect.android.widgets.utilities.FileRequester -import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry - -@SuppressLint("ViewConstructor") -class ExArbitraryFileWidget( - context: Context, - questionDetails: QuestionDetails, - private val widgetAnswerView: WidgetAnswerView, - questionMediaManager: QuestionMediaManager, - waitingForDataRegistry: WaitingForDataRegistry, - private val fileRequester: FileRequester, - dependencies: Dependencies -) : BaseArbitraryFileWidget( - context, - questionDetails, - questionMediaManager, - waitingForDataRegistry, - dependencies - ) { - lateinit var binding: ExArbitraryFileWidgetBinding - - init { - render() - } - - override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { - binding = ExArbitraryFileWidgetBinding.inflate((context as Activity).layoutInflater) - setupAnswerFile(prompt.answerText) - - if (questionDetails.isReadOnly) { - binding.exArbitraryFileButton.visibility = GONE - } else { - binding.exArbitraryFileButton.setOnClickListener { onButtonClick() } - binding.answerViewContainer.setOnClickListener { - mediaUtils.openFile( - getContext(), - answerFile!!, - null - ) - } - } - if (answerFile != null) { - widgetAnswerView.setAnswer(answerFile!!.name) - binding.answerViewContainer.visibility = VISIBLE - } else { - binding.answerViewContainer.visibility = GONE - } - binding.answerViewContainer.addView(widgetAnswerView) - - return binding.root - } - - override fun clearAnswer() { - binding.answerViewContainer.visibility = GONE - deleteFile() - widgetValueChanged() - } - - override fun setOnLongClickListener(listener: OnLongClickListener?) { - binding.exArbitraryFileButton.setOnLongClickListener(listener) - binding.answerViewContainer.setOnLongClickListener(listener) - } - - override fun showAnswerText() { - widgetAnswerView.setAnswer(answerFile!!.name) - binding.answerViewContainer.visibility = VISIBLE - } - - override fun hideAnswerText() { - binding.answerViewContainer.visibility = GONE - } - - private fun onButtonClick() { - waitingForDataRegistry.waitForData(formEntryPrompt.index) - fileRequester.launch( - (context as Activity), ApplicationConstants.RequestCodes.EX_ARBITRARY_FILE_CHOOSER, - formEntryPrompt - ) - } -} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/TextWidgetAnswer.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/TextWidgetAnswer.kt new file mode 100644 index 00000000000..cd19e8ad32a --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/TextWidgetAnswer.kt @@ -0,0 +1,66 @@ +package org.odk.collect.android.widgets + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.dimensionResource +import androidx.compose.ui.unit.sp +import org.odk.collect.androidshared.R.dimen +import org.odk.collect.androidshared.ui.multiclicksafe.MultiClickGuard + +@Composable +fun TextWidgetAnswer( + modifier: Modifier, + icon: ImageVector?, + answer: String, + fontSize: Int, + onLongClick: () -> Unit, + onClickLabel: String? = null, + onClick: () -> Unit = {} +) { + Row( + modifier = modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + if (MultiClickGuard.allowClick()) { + onClick() + } + }, + onLongClick = onLongClick, + onClickLabel = onClickLabel + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface.copy( + alpha = dimen.high_emphasis.toFloat() + ) + ) + Spacer(modifier = Modifier.width(dimensionResource(id = dimen.margin_small))) + } + Text( + text = answer, + style = MaterialTheme.typography.bodyLarge.copy( + fontSize = fontSize.sp, + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = dimen.high_emphasis.toFloat() + ) + ) + ) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswer.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswer.kt index c7025174667..20d645a2d9b 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswer.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetAnswer.kt @@ -1,24 +1,58 @@ package org.odk.collect.android.widgets +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AttachFile import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.lifecycle.ViewModelProvider import org.javarosa.core.model.Constants import org.javarosa.form.api.FormEntryPrompt +import org.odk.collect.android.widgets.arbitraryfile.ArbitraryFileWidgetAnswerViewModel import org.odk.collect.android.widgets.video.VideoWidgetAnswer +import org.odk.collect.icons.R @Composable fun WidgetAnswer( modifier: Modifier = Modifier, prompt: FormEntryPrompt, answer: String?, - viewModelProvider: ViewModelProvider, + fontSize: Int = 0, + viewModelProvider: ViewModelProvider? = null, onLongClick: () -> Unit = {} ) { if (answer != null) { when (prompt.controlType) { - Constants.CONTROL_VIDEO_CAPTURE -> VideoWidgetAnswer(modifier, answer, viewModelProvider, onLongClick) - else -> throw IllegalArgumentException("Unsupported control type: ${prompt.controlType}") + Constants.CONTROL_INPUT -> { + when (prompt.dataType) { + Constants.DATATYPE_BARCODE -> TextWidgetAnswer( + modifier, + ImageVector.vectorResource(R.drawable.ic_baseline_barcode_scanner_white_24), + answer, + fontSize, + onLongClick + ) + else -> TextWidgetAnswer(modifier, null, answer, fontSize, onLongClick) + } + } + Constants.CONTROL_VIDEO_CAPTURE -> VideoWidgetAnswer(modifier, answer, viewModelProvider!!, onLongClick) + Constants.CONTROL_FILE_CAPTURE -> { + val context = LocalContext.current + val viewModel = viewModelProvider!![ArbitraryFileWidgetAnswerViewModel::class] + + TextWidgetAnswer( + modifier, + Icons.Default.AttachFile, + answer, + fontSize, + onLongClick, + stringResource(org.odk.collect.strings.R.string.open_file) + ) { viewModel.openFile(context, answer) } + } + else -> TextWidgetAnswer(modifier, null, answer, fontSize, onLongClick) } } } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetFactory.java b/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetFactory.java index 2e4e08c4a87..ca882f1b6a0 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetFactory.java +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/WidgetFactory.java @@ -35,6 +35,9 @@ import org.odk.collect.android.storage.StoragePathProvider; import org.odk.collect.android.utilities.Appearances; import org.odk.collect.android.utilities.QuestionMediaManager; +import org.odk.collect.android.widgets.arbitraryfile.ArbitraryFileWidget; +import org.odk.collect.android.widgets.arbitraryfile.ExArbitraryFileWidget; +import org.odk.collect.android.widgets.barcode.BarcodeWidget; import org.odk.collect.android.widgets.datetime.DateTimeWidget; import org.odk.collect.android.widgets.datetime.DateWidget; import org.odk.collect.android.widgets.datetime.TimeWidget; @@ -62,7 +65,6 @@ import org.odk.collect.android.widgets.utilities.DateTimeWidgetUtils; import org.odk.collect.android.widgets.utilities.FileRequester; import org.odk.collect.android.widgets.utilities.GetContentAudioFileRequester; -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils; import org.odk.collect.android.widgets.utilities.RecordingRequester; import org.odk.collect.android.widgets.utilities.RecordingRequesterProvider; import org.odk.collect.android.widgets.utilities.StringRequester; @@ -71,7 +73,6 @@ import org.odk.collect.androidshared.system.IntentLauncherImpl; import org.odk.collect.audiorecorder.recording.AudioRecorder; import org.odk.collect.permissions.PermissionsProvider; -import org.odk.collect.settings.SettingsProvider; import org.odk.collect.webpage.CustomTabsWebPageService; /** @@ -97,7 +98,6 @@ public class WidgetFactory { private final StringRequester stringRequester; private final FormController formController; private final AdvanceToNextListener advanceToNextListener; - private final SettingsProvider settingsProvider; public WidgetFactory(Activity activity, boolean useExternalRecorder, @@ -112,8 +112,7 @@ public WidgetFactory(Activity activity, FileRequester fileRequester, StringRequester stringRequester, FormController formController, - AdvanceToNextListener advanceToNextListener, - SettingsProvider settingsProvider + AdvanceToNextListener advanceToNextListener ) { this.activity = activity; this.useExternalRecorder = useExternalRecorder; @@ -129,7 +128,6 @@ public WidgetFactory(Activity activity, this.stringRequester = stringRequester; this.formController = formController; this.advanceToNextListener = advanceToNextListener; - this.settingsProvider = settingsProvider; } public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, PermissionsProvider permissionsProvider) { @@ -140,7 +138,6 @@ public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, Permissions String appearance = Appearances.getSanitizedAppearanceHint(prompt); QuestionDetails questionDetails = new QuestionDetails(prompt, readOnlyOverride); QuestionWidget.Dependencies dependencies = new QuestionWidget.Dependencies(audioPlayer); - Integer answerFontSize = QuestionFontSizeUtils.getFontSize(settingsProvider.getUnprotectedSettings(), QuestionFontSizeUtils.FontSize.HEADLINE_6); final QuestionWidget questionWidget; switch (prompt.getControlType()) { @@ -192,7 +189,7 @@ public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, Permissions MapConfiguratorProvider.getConfigurator(), new ActivityGeoDataRequester(permissionsProvider, activity), dependencies); break; case Constants.DATATYPE_BARCODE: - questionWidget = new BarcodeWidget(activity, questionDetails, new BarcodeWidgetAnswerView(activity, answerFontSize), waitingForDataRegistry, new CameraUtils(), dependencies); + questionWidget = new BarcodeWidget(activity, questionDetails, dependencies, waitingForDataRegistry, new CameraUtils()); break; case Constants.DATATYPE_TEXT: String query = prompt.getQuestion().getAdditionalAttribute(null, "query"); @@ -217,9 +214,9 @@ public QuestionWidget createWidgetFromPrompt(FormEntryPrompt prompt, Permissions break; case Constants.CONTROL_FILE_CAPTURE: if (appearance.startsWith(Appearances.EX)) { - questionWidget = new ExArbitraryFileWidget(activity, questionDetails, new ArbitraryFileWidgetAnswerView(activity, answerFontSize), questionMediaManager, waitingForDataRegistry, fileRequester, dependencies); + questionWidget = new ExArbitraryFileWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry, fileRequester); } else { - questionWidget = new ArbitraryFileWidget(activity, questionDetails, new ArbitraryFileWidgetAnswerView(activity, answerFontSize), questionMediaManager, waitingForDataRegistry, dependencies); + questionWidget = new ArbitraryFileWidget(activity, questionDetails, dependencies, questionMediaManager, waitingForDataRegistry); } break; case Constants.CONTROL_IMAGE_CHOOSE: diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidget.kt new file mode 100644 index 00000000000..940f54a1088 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidget.kt @@ -0,0 +1,108 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.view.View +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.dimensionResource +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.viewModelFactory +import org.javarosa.form.api.FormEntryPrompt +import org.odk.collect.android.formentry.questions.QuestionDetails +import org.odk.collect.android.utilities.ApplicationConstants +import org.odk.collect.android.utilities.QuestionMediaManager +import org.odk.collect.android.widgets.QuestionWidget +import org.odk.collect.android.widgets.WidgetAnswer +import org.odk.collect.android.widgets.interfaces.FileWidget +import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver +import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils +import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry +import org.odk.collect.androidshared.R.dimen +import org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent + +@SuppressLint("ViewConstructor") +class ArbitraryFileWidget( + context: Context, + questionDetails: QuestionDetails, + dependencies: Dependencies, + private val questionMediaManager: QuestionMediaManager, + private val waitingForDataRegistry: WaitingForDataRegistry +) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver { + private val arbitraryFileWidgetDelegate = ArbitraryFileWidgetDelegate(questionMediaManager) + private var answer by mutableStateOf(formEntryPrompt.answerText) + + init { + render() + } + + override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { + val viewModelProvider = ViewModelProvider( + context as ComponentActivity, + viewModelFactory { + addInitializer(ArbitraryFileWidgetAnswerViewModel::class) { + ArbitraryFileWidgetAnswerViewModel(questionMediaManager, mediaUtils) + } + } + ) + + return ComposeView(context).apply { + val readOnly = questionDetails.isReadOnly + val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE) + + setContextThemedContent { + ArbitraryFileWidgetContent( + readOnly, + buttonFontSize, + onChooseFileClick = { onButtonClick() }, + onLongClick = { showContextMenu() } + ) { + WidgetAnswer( + Modifier.padding(top = dimensionResource(id = dimen.margin_standard)), + formEntryPrompt, + answer, + answerFontSize, + viewModelProvider, + onLongClick = { showContextMenu() } + ) + } + } + } + } + + override fun getAnswer() = arbitraryFileWidgetDelegate.getAnswer(answer) + + override fun deleteFile() { + arbitraryFileWidgetDelegate.deleteFile(formEntryPrompt.index.toString(), answer) + answer = null + } + + override fun setData(answer: Any) { + arbitraryFileWidgetDelegate.setData(formEntryPrompt.index.toString(), this.answer, answer) { + this.answer = it + widgetValueChanged() + } + } + + override fun clearAnswer() { + deleteFile() + widgetValueChanged() + } + + private fun onButtonClick() { + waitingForDataRegistry.waitForData(formEntryPrompt.index) + mediaUtils.pickFile( + (context as Activity), + "*/*", + ApplicationConstants.RequestCodes.ARBITRARY_FILE_CHOOSER + ) + } + + override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetAnswerViewModel.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetAnswerViewModel.kt new file mode 100644 index 00000000000..5811d9e800c --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetAnswerViewModel.kt @@ -0,0 +1,19 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import android.content.Context +import androidx.lifecycle.ViewModel +import org.odk.collect.android.utilities.MediaUtils +import org.odk.collect.android.utilities.QuestionMediaManager + +class ArbitraryFileWidgetAnswerViewModel( + private val questionMediaManager: QuestionMediaManager, + private val mediaUtils: MediaUtils +) : ViewModel() { + + fun openFile(context: Context, answer: String?) { + val file = questionMediaManager.getAnswerFile(answer) + if (file != null) { + mediaUtils.openFile(context, file, null) + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetContent.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetContent.kt new file mode 100644 index 00000000000..5ee62032b63 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetContent.kt @@ -0,0 +1,48 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.odk.collect.android.widgets.WidgetIconButton +import org.odk.collect.strings.R.string + +@Composable +fun ArbitraryFileWidgetContent( + readOnly: Boolean, + fontSize: Int, + onChooseFileClick: () -> Unit, + onLongClick: () -> Unit, + widgetAnswer: @Composable () -> Unit +) { + Column { + if (!readOnly) { + WidgetIconButton( + Icons.Default.AttachFile, + stringResource(string.choose_file), + fontSize, + onChooseFileClick, + onLongClick + ) + } + + widgetAnswer() + } +} + +@Preview +@Composable +private fun ArbitraryFileWidgetContentPreview() { + MaterialTheme { + ArbitraryFileWidgetContent( + false, + 10, + {}, + {}, + {} + ) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetDelegate.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetDelegate.kt new file mode 100644 index 00000000000..d04228dcfb5 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ArbitraryFileWidgetDelegate.kt @@ -0,0 +1,47 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import org.javarosa.core.model.data.IAnswerData +import org.javarosa.core.model.data.StringData +import org.odk.collect.android.utilities.QuestionMediaManager +import timber.log.Timber +import java.io.File + +class ArbitraryFileWidgetDelegate( + private val questionMediaManager: QuestionMediaManager +) { + fun getAnswer(answer: String?): IAnswerData? { + return if (answer.isNullOrEmpty()) null else StringData(answer) + } + + fun deleteFile(formEntryPromptIndex: String, answer: String?) { + questionMediaManager.deleteAnswerFile( + formEntryPromptIndex, + questionMediaManager.getAnswerFile(answer)!!.absolutePath + ) + } + + fun setData( + formEntryPromptIndex: String, + previousAnswer: String?, + newAnswer: Any, + onSuccess: (String) -> Unit + ) { + if (previousAnswer != null) { + deleteFile(formEntryPromptIndex, previousAnswer) + } + + if (newAnswer is File) { + if (newAnswer.exists()) { + questionMediaManager.replaceAnswerFile( + formEntryPromptIndex, + newAnswer.absolutePath + ) + onSuccess(newAnswer.name) + } else { + Timber.e(Error("Inserting Arbitrary file FAILED")) + } + } else { + Timber.e(Error("FileWidget's setBinaryData must receive a File but received: " + newAnswer.javaClass)) + } + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidget.kt new file mode 100644 index 00000000000..2fdea6f135c --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidget.kt @@ -0,0 +1,109 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.view.View +import androidx.activity.ComponentActivity +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.dimensionResource +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.viewModelFactory +import org.javarosa.form.api.FormEntryPrompt +import org.odk.collect.android.formentry.questions.QuestionDetails +import org.odk.collect.android.utilities.ApplicationConstants +import org.odk.collect.android.utilities.QuestionMediaManager +import org.odk.collect.android.widgets.QuestionWidget +import org.odk.collect.android.widgets.WidgetAnswer +import org.odk.collect.android.widgets.interfaces.FileWidget +import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver +import org.odk.collect.android.widgets.utilities.FileRequester +import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils +import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry +import org.odk.collect.androidshared.R.dimen +import org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent + +@SuppressLint("ViewConstructor") +class ExArbitraryFileWidget( + context: Context, + questionDetails: QuestionDetails, + dependencies: Dependencies, + private val questionMediaManager: QuestionMediaManager, + private val waitingForDataRegistry: WaitingForDataRegistry, + private val fileRequester: FileRequester, +) : QuestionWidget(context, dependencies, questionDetails), FileWidget, WidgetDataReceiver { + private val arbitraryFileWidgetDelegate = ArbitraryFileWidgetDelegate(questionMediaManager) + private var answer by mutableStateOf(formEntryPrompt.answerText) + + init { + render() + } + + override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { + val viewModelProvider = ViewModelProvider( + context as ComponentActivity, + viewModelFactory { + addInitializer(ArbitraryFileWidgetAnswerViewModel::class) { + ArbitraryFileWidgetAnswerViewModel(questionMediaManager, mediaUtils) + } + } + ) + + return ComposeView(context).apply { + val readOnly = questionDetails.isReadOnly + val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE) + + setContextThemedContent { + ExArbitraryFileWidgetContent( + readOnly, + buttonFontSize, + onLaunchClick = { onButtonClick() }, + onLongClick = { showContextMenu() } + ) { + WidgetAnswer( + Modifier.padding(top = dimensionResource(id = dimen.margin_standard)), + formEntryPrompt, + answer, + answerFontSize, + viewModelProvider, + onLongClick = { showContextMenu() } + ) + } + } + } + } + + override fun getAnswer() = arbitraryFileWidgetDelegate.getAnswer(answer) + + override fun deleteFile() { + arbitraryFileWidgetDelegate.deleteFile(formEntryPrompt.index.toString(), answer) + answer = null + } + + override fun setData(answer: Any) { + arbitraryFileWidgetDelegate.setData(formEntryPrompt.index.toString(), this.answer, answer) { + this.answer = it + widgetValueChanged() + } + } + + override fun clearAnswer() { + deleteFile() + widgetValueChanged() + } + + override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit + + private fun onButtonClick() { + waitingForDataRegistry.waitForData(formEntryPrompt.index) + fileRequester.launch( + (context as Activity), ApplicationConstants.RequestCodes.EX_ARBITRARY_FILE_CHOOSER, + formEntryPrompt + ) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidgetContent.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidgetContent.kt new file mode 100644 index 00000000000..2d3a59f2ad1 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/arbitraryfile/ExArbitraryFileWidgetContent.kt @@ -0,0 +1,48 @@ +package org.odk.collect.android.widgets.arbitraryfile + +import androidx.compose.foundation.layout.Column +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import org.odk.collect.android.widgets.WidgetIconButton +import org.odk.collect.strings.R.string + +@Composable +fun ExArbitraryFileWidgetContent( + readOnly: Boolean, + fontSize: Int, + onLaunchClick: () -> Unit, + onLongClick: () -> Unit, + widgetAnswer: @Composable () -> Unit +) { + Column { + if (!readOnly) { + WidgetIconButton( + Icons.AutoMirrored.Filled.OpenInNew, + stringResource(string.launch_app), + fontSize, + onLaunchClick, + onLongClick + ) + } + + widgetAnswer() + } +} + +@Preview +@Composable +private fun ExArbitraryFileWidgetContentPreview() { + MaterialTheme { + ExArbitraryFileWidgetContent( + false, + 10, + {}, + {}, + {} + ) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidget.kt similarity index 60% rename from collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt rename to collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidget.kt index 51a332a5bc7..fe4d12e044c 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidget.kt @@ -1,22 +1,33 @@ -package org.odk.collect.android.widgets +package org.odk.collect.android.widgets.barcode import android.annotation.SuppressLint import android.app.Activity import android.content.Context import android.view.View +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.dimensionResource import com.google.zxing.integration.android.IntentIntegrator import org.javarosa.core.model.data.IAnswerData import org.javarosa.core.model.data.StringData import org.javarosa.form.api.FormEntryPrompt import org.odk.collect.android.activities.ScannerWithFlashlightActivity -import org.odk.collect.android.databinding.BarcodeWidgetBinding import org.odk.collect.android.formentry.questions.QuestionDetails import org.odk.collect.android.utilities.Appearances import org.odk.collect.android.utilities.Appearances.hasAppearance import org.odk.collect.android.utilities.Appearances.isFrontCameraAppearance +import org.odk.collect.android.widgets.QuestionWidget +import org.odk.collect.android.widgets.WidgetAnswer import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver +import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry +import org.odk.collect.androidshared.R.dimen import org.odk.collect.androidshared.system.CameraUtils +import org.odk.collect.androidshared.ui.ComposeThemeProvider.Companion.setContextThemedContent import org.odk.collect.androidshared.ui.ToastUtils.showLongToast import org.odk.collect.permissions.PermissionListener import org.odk.collect.strings.R @@ -25,44 +36,45 @@ import org.odk.collect.strings.R class BarcodeWidget( context: Context, questionDetails: QuestionDetails, - private val widgetAnswerView: WidgetAnswerView, + dependencies: Dependencies, private val waitingForDataRegistry: WaitingForDataRegistry, - private val cameraUtils: CameraUtils, - dependencies: Dependencies + private val cameraUtils: CameraUtils ) : QuestionWidget(context, dependencies, questionDetails), WidgetDataReceiver { - lateinit var binding: BarcodeWidgetBinding - - private var answer: String? = null + private var answer by mutableStateOf(formEntryPrompt.answerText) init { render() } override fun onCreateWidgetView(context: Context, prompt: FormEntryPrompt, answerFontSize: Int): View { - binding = BarcodeWidgetBinding.inflate((context as Activity).layoutInflater) - - if (prompt.isReadOnly) { - binding.barcodeButton.visibility = GONE - } else { - binding.barcodeButton.setOnClickListener { onButtonClick() } - } - - answer = prompt.answerText - if (!answer.isNullOrEmpty()) { - binding.barcodeButton.text = getContext().getString(R.string.replace_barcode) + return ComposeView(context).apply { + val readOnly = questionDetails.isReadOnly + val isAnswerHidden = hasAppearance(formEntryPrompt, Appearances.HIDDEN_ANSWER) + val buttonFontSize = QuestionFontSizeUtils.getFontSize(settings, QuestionFontSizeUtils.FontSize.BODY_LARGE) + + setContextThemedContent { + BarcodeWidgetContent( + answer, + readOnly, + isAnswerHidden, + buttonFontSize, + onGetBarcodeClick = { onButtonClick() }, + onLongClick = { showContextMenu() } + ) { + WidgetAnswer( + Modifier.padding(top = dimensionResource(id = dimen.margin_standard)), + formEntryPrompt, + answer, + answerFontSize, + onLongClick = { showContextMenu() } + ) + } + } } - widgetAnswerView.setAnswer(answer) - binding.answerViewContainer.addView(widgetAnswerView) - updateAnswerVisibility() - - return binding.root } override fun clearAnswer() { answer = null - widgetAnswerView.setAnswer(null) - binding.barcodeButton.text = context.getString(R.string.get_barcode) - updateAnswerVisibility() widgetValueChanged() } @@ -72,27 +84,10 @@ class BarcodeWidget( override fun setData(answer: Any) { this.answer = stripInvalidCharacters(answer as String) - widgetAnswerView.setAnswer(this.answer) - binding.barcodeButton.text = context.getString(R.string.replace_barcode) - updateAnswerVisibility() widgetValueChanged() } - override fun setOnLongClickListener(l: OnLongClickListener?) { - binding.barcodeButton.setOnLongClickListener(l) - binding.answerViewContainer.setOnLongClickListener(l) - } - - override fun cancelLongPress() { - super.cancelLongPress() - binding.barcodeButton.cancelLongPress() - binding.answerViewContainer.cancelLongPress() - } - - private fun updateAnswerVisibility() { - val isAnswerHidden = hasAppearance(formEntryPrompt, Appearances.HIDDEN_ANSWER) - binding.answerViewContainer.visibility = if (isAnswerHidden || answer.isNullOrEmpty()) GONE else VISIBLE - } + override fun setOnLongClickListener(listener: OnLongClickListener?) = Unit private fun onButtonClick() { getPermissionsProvider().requestCameraPermission( diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidgetContent.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidgetContent.kt new file mode 100644 index 00000000000..8487698f6a3 --- /dev/null +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/barcode/BarcodeWidgetContent.kt @@ -0,0 +1,75 @@ +package org.odk.collect.android.widgets.barcode + +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import org.odk.collect.android.widgets.WidgetIconButton +import org.odk.collect.icons.R +import org.odk.collect.strings.R.string + +@Composable +fun BarcodeWidgetContent( + answer: String?, + readOnly: Boolean, + isAnswerHidden: Boolean, + fontSize: Int, + onGetBarcodeClick: () -> Unit, + onLongClick: () -> Unit, + widgetAnswer: @Composable () -> Unit +) { + Column { + if (!readOnly) { + WidgetIconButton( + ImageVector.vectorResource(R.drawable.ic_baseline_barcode_scanner_white_24), + if (answer == null) { + stringResource(string.get_barcode) + } else { + stringResource(string.replace_barcode) + }, + fontSize, + onGetBarcodeClick, + onLongClick + ) + } + + if (!isAnswerHidden) { + widgetAnswer() + } + } +} + +@Preview +@Composable +private fun BarcodeWidgetContentNoAnswerPreview() { + MaterialTheme { + BarcodeWidgetContent( + null, + false, + false, + 10, + {}, + {}, + {} + ) + } +} + +@Preview +@Composable +private fun BarcodeWidgetContentWithAnswerPreview() { + MaterialTheme { + BarcodeWidgetContent( + "123", + false, + false, + 10, + {}, + {}, + {} + ) + } +} diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidget.kt index cbd53ab21d3..9513b2e2ffb 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidget.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/video/ExVideoWidget.kt @@ -74,7 +74,7 @@ class ExVideoWidget( Modifier.padding(top = dimensionResource(id = dimen.margin_standard)), formEntryPrompt, binaryName, - viewModelProvider, + viewModelProvider = viewModelProvider, onLongClick = { showContextMenu() } ) } diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidget.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidget.kt index 2b3e85c5c40..d90aebb4fc7 100644 --- a/collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidget.kt +++ b/collect_app/src/main/java/org/odk/collect/android/widgets/video/VideoWidget.kt @@ -89,7 +89,7 @@ class VideoWidget( Modifier.padding(top = dimensionResource(id = dimen.margin_standard)), formEntryPrompt, binaryName, - viewModelProvider, + viewModelProvider = viewModelProvider, onLongClick = { showContextMenu() } ) } diff --git a/collect_app/src/main/res/layout/arbitrary_file_widget.xml b/collect_app/src/main/res/layout/arbitrary_file_widget.xml deleted file mode 100644 index fb011a91071..00000000000 --- a/collect_app/src/main/res/layout/arbitrary_file_widget.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/collect_app/src/main/res/layout/arbitrary_file_widget_answer_view.xml b/collect_app/src/main/res/layout/arbitrary_file_widget_answer_view.xml deleted file mode 100644 index db632194b9e..00000000000 --- a/collect_app/src/main/res/layout/arbitrary_file_widget_answer_view.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/collect_app/src/main/res/layout/barcode_widget.xml b/collect_app/src/main/res/layout/barcode_widget.xml deleted file mode 100644 index 141bd9b8aae..00000000000 --- a/collect_app/src/main/res/layout/barcode_widget.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/collect_app/src/main/res/layout/barcode_widget_answer_view.xml b/collect_app/src/main/res/layout/barcode_widget_answer_view.xml deleted file mode 100644 index 1a2f112d2e3..00000000000 --- a/collect_app/src/main/res/layout/barcode_widget_answer_view.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/collect_app/src/main/res/layout/ex_arbitrary_file_widget.xml b/collect_app/src/main/res/layout/ex_arbitrary_file_widget.xml deleted file mode 100644 index 5f7e1ea0a8f..00000000000 --- a/collect_app/src/main/res/layout/ex_arbitrary_file_widget.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ArbitraryFileWidgetTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/ArbitraryFileWidgetTest.kt index 8e3f78ae390..1c6620f06ca 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ArbitraryFileWidgetTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ArbitraryFileWidgetTest.kt @@ -1,10 +1,14 @@ package org.odk.collect.android.widgets -import android.view.View +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.javarosa.core.model.Constants import org.javarosa.core.model.data.StringData import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -13,22 +17,26 @@ import org.mockito.kotlin.whenever import org.odk.collect.android.formentry.questions.QuestionDetails import org.odk.collect.android.injection.config.AppDependencyModule import org.odk.collect.android.support.CollectHelpers +import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.support.WidgetTestActivity import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.MediaUtils +import org.odk.collect.android.widgets.arbitraryfile.ArbitraryFileWidget import org.odk.collect.android.widgets.base.FileWidgetTest import org.odk.collect.android.widgets.support.FakeQuestionMediaManager import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry -import org.odk.collect.android.widgets.support.QuestionWidgetHelpers -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils.getFontSize import org.odk.collect.androidshared.system.IntentLauncher -import org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE +import org.odk.collect.androidtest.onNodeWithClickLabel +import org.odk.collect.strings.R.string +import java.io.File -class ArbitraryFileWidgetTest : FileWidgetTest() { +class ArbitraryFileWidgetTest : FileWidgetTest() { + @get:Rule + val composeRule = createAndroidComposeRule() private val mediaUtils = mock().also { whenever(it.isAudioFile(any())).thenReturn(true) } - private val widgetAnswer = ArbitraryFileWidgetAnswerView(QuestionWidgetHelpers.widgetTestActivity(), 5) + private val questionMediaManager = FakeQuestionMediaManager() @Before fun setup() { @@ -37,6 +45,9 @@ class ArbitraryFileWidgetTest : FileWidgetTest() { return mediaUtils } }) + formEntryPrompt = MockFormEntryPromptBuilder() + .withControlType(Constants.CONTROL_FILE_CAPTURE) + .build() } override fun getInitialAnswer(): StringData { @@ -49,83 +60,94 @@ class ArbitraryFileWidgetTest : FileWidgetTest() { override fun createWidget(): ArbitraryFileWidget { return ArbitraryFileWidget( - activity, QuestionDetails(formEntryPrompt, readOnlyOverride), widgetAnswer, - FakeQuestionMediaManager(), FakeWaitingForDataRegistry(), dependencies - ) - } - - @Test - fun `Use custom font size when font size changes`() { - settingsProvider.getUnprotectedSettings().save(KEY_FONT_SIZE, "30") - - assertThat( - widget!!.binding.arbitraryFileButton.textSize.toInt(), equalTo( - getFontSize( - settingsProvider.getUnprotectedSettings(), - QuestionFontSizeUtils.FontSize.BODY_LARGE - ) - ) - ) - } - - @Test - fun `Hide the answer text when there is no answer`() { - assertThat(widget!!.binding.answerViewContainer.visibility, equalTo(View.GONE)) + composeRule.activity, + QuestionDetails(formEntryPrompt, readOnlyOverride), + dependencies, + questionMediaManager, + FakeWaitingForDataRegistry() + ).also { + composeRule.activity.setContentView(it) + activity = composeRule.activity + } } @Test fun `Display the answer text when there is answer`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) - assertThat(widget.answer!!.displayText, equalTo(initialAnswer.displayText)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + createWidget() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() } @Test fun `File picker should be called when clicking on button`() { - widget!!.binding.arbitraryFileButton.performClick() + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).performClick() verify(mediaUtils).pickFile(activity, "*/*", ApplicationConstants.RequestCodes.ARBITRARY_FILE_CHOOSER) } @Test fun `File viewer should be called when clicking on answer`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - widget.binding.answerViewContainer.performClick() - verify(mediaUtils).openFile(activity, widget.answerFile!!, null) + val file = questionMediaManager.addAnswerFile(File.createTempFile("document", ".pdf")) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(file.name)) + .build() + + createWidget() + composeRule.onNodeWithText(file.name).performClick() + verify(mediaUtils).openFile(activity, file, null) } @Test fun `Hide the answer when clear answer is called`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(initialAnswer.displayText)) + .build() - val widget = widget!! + val widget = createWidget() widget.clearAnswer() - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.GONE)) + composeRule.onNodeWithText(initialAnswer.displayText).assertDoesNotExist() } @Test fun `All clickable elements should be disabled when read-only override option is used`() { readOnlyOverride = true - whenever(formEntryPrompt.isReadOnly).thenReturn(false) - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - assertThat(widget.binding.arbitraryFileButton.visibility, equalTo(View.GONE)) - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) - assertThat(widget.binding.answerViewContainer.hasOnClickListeners(), equalTo(true)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(false) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).assertDoesNotExist() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() } @Test override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { - whenever(formEntryPrompt.isReadOnly).thenReturn(true) - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(true) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.choose_file)).assertDoesNotExist() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() + } - val widget = widget!! - assertThat(widget.binding.arbitraryFileButton.visibility, equalTo(View.GONE)) - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) - assertThat(widget.binding.answerViewContainer.hasOnClickListeners(), equalTo(true)) + @Test + override fun settingANewAnswerShouldCallDeleteMediaToRemoveTheOldFile() { + val file = questionMediaManager.addAnswerFile(File.createTempFile("document", ".pdf")) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(file.name)) + .build() + + val widget = createWidget() + widget.setData(createBinaryData(nextAnswer)) + + assertThat( + questionMediaManager.originalFiles[formEntryPrompt.index.toString()], + equalTo(file.absolutePath) + ) } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt index 7d2a0519679..845b40eeae3 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt @@ -1,15 +1,16 @@ package org.odk.collect.android.widgets -import android.view.View -import android.view.View.OnLongClickListener -import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import net.bytebuddy.utility.RandomString import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat -import org.javarosa.core.model.FormIndex +import org.javarosa.core.model.Constants import org.javarosa.core.model.data.StringData -import org.javarosa.form.api.FormEntryPrompt +import org.junit.Before +import org.junit.Rule import org.junit.Test -import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.Mockito.verify import org.mockito.kotlin.mock @@ -17,88 +18,115 @@ import org.mockito.kotlin.whenever import org.odk.collect.android.fakes.FakePermissionsProvider import org.odk.collect.android.formentry.questions.QuestionDetails import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.support.WidgetTestActivity import org.odk.collect.android.utilities.Appearances +import org.odk.collect.android.widgets.barcode.BarcodeWidget +import org.odk.collect.android.widgets.base.QuestionWidgetTest import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry import org.odk.collect.android.widgets.support.QuestionWidgetHelpers -import org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetDependencies import org.odk.collect.androidshared.system.CameraUtils +import org.odk.collect.androidtest.onNodeWithClickLabel import org.odk.collect.strings.R.string import org.robolectric.Shadows import org.robolectric.shadows.ShadowToast -@RunWith(AndroidJUnit4::class) -class BarcodeWidgetTest { +class BarcodeWidgetTest : QuestionWidgetTest() { + @get:Rule + val composeRule = createAndroidComposeRule() private val waitingForDataRegistry = FakeWaitingForDataRegistry() private val permissionsProvider = FakePermissionsProvider().apply { setPermissionGranted(true) } - private val widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity() - private val barcodeWidgetAnswer = BarcodeWidgetAnswerView(widgetTestActivity, 5) - private val shadowActivity = Shadows.shadowOf(widgetTestActivity) private val cameraUtils = mock() - private val listener = mock() - private val formIndex = mock() + + override fun createWidget(): BarcodeWidget { + return BarcodeWidget( + composeRule.activity, + QuestionDetails(formEntryPrompt), + dependencies, + waitingForDataRegistry, + cameraUtils + ).also { + composeRule.activity.setContentView(it) + activity = composeRule.activity + } + } + + override fun getNextAnswer(): StringData? { + return StringData(RandomString.make()) + } + + @Before + fun setup() { + formEntryPrompt = MockFormEntryPromptBuilder() + .withControlType(Constants.CONTROL_INPUT) + .withDataType(Constants.DATATYPE_BARCODE) + .build() + } @Test fun `The button is hidden in read-only mode`() { - assertThat( - createWidget(QuestionWidgetHelpers.promptWithReadOnly()).binding.barcodeButton.visibility, - equalTo(View.GONE) - ) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(true) + .build() + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertDoesNotExist() } @Test fun `Display the 'Replace Barcode' button if answer is present`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) - assertThat( - widget.binding.barcodeButton.text.toString(), - equalTo(widgetTestActivity.getString(string.replace_barcode)) - ) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData("blah")) + .build() + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertExists() } @Test fun `Display the answer if answer is present`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) - assertThat( - widget.answer!!.displayText, - equalTo("blah") - ) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData("blah")) + .build() + createWidget() + composeRule.onNodeWithText("blah").assertExists() } @Test fun `#getAnswer returns null when there is no answer`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + val widget = createWidget() assertThat(widget.answer, equalTo(null)) } @Test fun `#getAnswer returns the answer when there is answer`() { - val widget = createWidget( - QuestionWidgetHelpers.promptWithAnswer( - StringData("blah") - ) - ) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData("blah")) + .build() + val widget = createWidget() assertThat(widget.answer!!.displayText, equalTo("blah")) } @Test fun `#clearAnswer removes answer and updates button title`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData("blah")) + .build() + val widget = createWidget() widget.clearAnswer() assertThat( widget.answer, equalTo(null) ) - assertThat( - widget.binding.barcodeButton.text.toString(), - equalTo(widgetTestActivity.getString(string.get_barcode)) - ) + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertExists() } @Test fun `#clearAnswer calls #valueChangeListener`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah"))) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData("blah")) + .build() + val widget = createWidget() val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget) widget.clearAnswer() @@ -107,7 +135,7 @@ class BarcodeWidgetTest { @Test fun `#setData displays sanitized answer`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + val widget = createWidget() widget.setData("\ud800blah\b") assertThat( widget.answer!!.displayText, @@ -117,53 +145,38 @@ class BarcodeWidgetTest { @Test fun `#setData updates the button title`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + val widget = createWidget() widget.setData("\ud800blah\b") - assertThat( - widget.binding.barcodeButton.text, - equalTo(widgetTestActivity.getString(string.replace_barcode)) - ) + composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertExists() } @Test - fun `#setData call #valueChangeListener`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + fun `#setData calls #valueChangeListener`() { + val widget = createWidget() val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget) widget.setData("blah") verify(valueChangedListener).widgetValueChanged(widget) } - @Test - fun `Long-pressing the button and the answer triggers #onLongClickListener`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) - widget.setOnLongClickListener(listener) - widget.binding.barcodeButton.performLongClick() - widget.binding.answerViewContainer.performLongClick() - - verify(listener).onLongClick(widget.binding.barcodeButton) - verify(listener).onLongClick(widget.binding.answerViewContainer) - } - @Test fun `pressing the button with permission not granted does not launch anything`() { - val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null)) + val widget = createWidget() permissionsProvider.setPermissionGranted(false) widget.setPermissionsProvider(permissionsProvider) - widget.binding.barcodeButton.performClick() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick() - assertThat(shadowActivity.nextStartedActivity, equalTo(null)) + assertThat(Shadows.shadowOf(activity).nextStartedActivity, equalTo(null)) assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true)) } @Test fun `pressing the button with permission granted registers widget for data waiting`() { - val prompt = QuestionWidgetHelpers.promptWithAnswer(null) - whenever(prompt.index).thenReturn(formIndex) + whenever(formEntryPrompt.index).thenReturn(formIndex) - val widget = createWidget(prompt) + val widget = createWidget() widget.setPermissionsProvider(permissionsProvider) - widget.binding.barcodeButton.performClick() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick() assertThat( waitingForDataRegistry.waiting.contains(formIndex), @@ -174,71 +187,64 @@ class BarcodeWidgetTest { @Test fun `pressing the button when front camera should be used but it is not available displays a toast`() { whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(false) - val widget = createWidget(QuestionWidgetHelpers.promptWithAppearance(Appearances.FRONT)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAppearance(Appearances.FRONT) + .build() + val widget = createWidget() widget.setPermissionsProvider(permissionsProvider) - widget.binding.barcodeButton.performClick() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick() assertThat( ShadowToast.getTextOfLatestToast(), - equalTo(widgetTestActivity.getString(string.error_front_camera_unavailable)) + equalTo(activity.getString(string.error_front_camera_unavailable)) ) } @Test fun `pressing the button when front camera should be used and it is available launches correct intent`() { whenever(cameraUtils.isFrontCameraAvailable(ArgumentMatchers.any())).thenReturn(true) - val widget = createWidget(QuestionWidgetHelpers.promptWithAppearance(Appearances.FRONT)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAppearance(Appearances.FRONT) + .build() + val widget = createWidget() widget.setPermissionsProvider(permissionsProvider) - widget.binding.barcodeButton.performClick() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).performClick() assertThat( - shadowActivity.nextStartedActivity.getBooleanExtra(Appearances.FRONT, false), + Shadows.shadowOf(activity).nextStartedActivity.getBooleanExtra(Appearances.FRONT, false), equalTo(true) ) } @Test fun `The answer is not displayed with hidden mode`() { - val prompt = - MockFormEntryPromptBuilder(QuestionWidgetHelpers.promptWithAppearance(Appearances.HIDDEN_ANSWER)) - .withAnswer(StringData("original contents")) - .build() + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAppearance(Appearances.HIDDEN_ANSWER) + .withAnswer(StringData("original contents")) + .build() - val widget = createWidget(prompt) + val widget = createWidget() // Check initial value is not shown - assertThat( - widget.binding.answerViewContainer.visibility, - equalTo(View.GONE) - ) - assertThat( - widget.binding.barcodeButton.text, - equalTo(widgetTestActivity.getString(string.replace_barcode)) - ) + composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertExists() + composeRule.onNodeWithText("original contents").assertDoesNotExist() assertThat(widget.answer, equalTo(StringData("original contents"))) // Check updates aren't shown widget.setData("updated contents") - assertThat( - widget.binding.answerViewContainer.visibility, - equalTo(View.GONE) - ) - assertThat( - widget.binding.barcodeButton.text, - equalTo(widgetTestActivity.getString(string.replace_barcode)) - ) + composeRule.onNodeWithClickLabel(activity.getString(string.replace_barcode)).assertExists() + composeRule.onNodeWithText("updated contents").assertDoesNotExist() assertThat( widget.answer, equalTo(StringData("updated contents")) ) } - private fun createWidget(prompt: FormEntryPrompt?) = BarcodeWidget( - widgetTestActivity, - QuestionDetails(prompt), - barcodeWidgetAnswer, - waitingForDataRegistry, - cameraUtils, - widgetDependencies() - ) + override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(true) + .build() + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.get_barcode)).assertDoesNotExist() + } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/ExArbitraryFileWidgetTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/ExArbitraryFileWidgetTest.kt index 1c00633ae9c..c9391306b6f 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/ExArbitraryFileWidgetTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/ExArbitraryFileWidgetTest.kt @@ -1,10 +1,14 @@ package org.odk.collect.android.widgets -import android.view.View +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.MatcherAssert.assertThat +import org.javarosa.core.model.Constants import org.javarosa.core.model.data.StringData import org.junit.Before +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.mock @@ -13,24 +17,28 @@ import org.mockito.kotlin.whenever import org.odk.collect.android.formentry.questions.QuestionDetails import org.odk.collect.android.injection.config.AppDependencyModule import org.odk.collect.android.support.CollectHelpers +import org.odk.collect.android.support.MockFormEntryPromptBuilder +import org.odk.collect.android.support.WidgetTestActivity import org.odk.collect.android.utilities.ApplicationConstants import org.odk.collect.android.utilities.MediaUtils +import org.odk.collect.android.widgets.arbitraryfile.ExArbitraryFileWidget import org.odk.collect.android.widgets.base.FileWidgetTest import org.odk.collect.android.widgets.support.FakeQuestionMediaManager import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry -import org.odk.collect.android.widgets.support.QuestionWidgetHelpers import org.odk.collect.android.widgets.utilities.FileRequester -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils.getFontSize import org.odk.collect.androidshared.system.IntentLauncher -import org.odk.collect.settings.keys.ProjectKeys.KEY_FONT_SIZE +import org.odk.collect.androidtest.onNodeWithClickLabel +import org.odk.collect.strings.R.string +import java.io.File -class ExArbitraryFileWidgetTest : FileWidgetTest() { +class ExArbitraryFileWidgetTest : FileWidgetTest() { + @get:Rule + val composeRule = createAndroidComposeRule() private val fileRequester = mock() private val mediaUtils = mock().also { whenever(it.isAudioFile(any())).thenReturn(true) } - private val widgetAnswer = ArbitraryFileWidgetAnswerView(QuestionWidgetHelpers.widgetTestActivity(), 5) + private val questionMediaManager = FakeQuestionMediaManager() @Before fun setup() { @@ -39,6 +47,9 @@ class ExArbitraryFileWidgetTest : FileWidgetTest() { return mediaUtils } }) + formEntryPrompt = MockFormEntryPromptBuilder() + .withControlType(Constants.CONTROL_FILE_CAPTURE) + .build() } override fun getInitialAnswer(): StringData { @@ -51,81 +62,95 @@ class ExArbitraryFileWidgetTest : FileWidgetTest() { override fun createWidget(): ExArbitraryFileWidget { return ExArbitraryFileWidget( - activity, QuestionDetails(formEntryPrompt, readOnlyOverride), widgetAnswer, - FakeQuestionMediaManager(), FakeWaitingForDataRegistry(), fileRequester, dependencies - ) - } - - @Test - fun `Use custom font size when font size changes`() { - settingsProvider.getUnprotectedSettings().save(KEY_FONT_SIZE, "30") - - assertThat( - widget!!.binding.exArbitraryFileButton.textSize.toInt(), equalTo( - getFontSize( - settingsProvider.getUnprotectedSettings(), - QuestionFontSizeUtils.FontSize.BODY_LARGE - ) - ) - ) - } - - @Test - fun `Hide the answer text when there is no answer`() { - assertThat(widget!!.binding.answerViewContainer.visibility, equalTo(View.GONE)) + composeRule.activity, + QuestionDetails(formEntryPrompt, readOnlyOverride), + dependencies, + questionMediaManager, + FakeWaitingForDataRegistry(), + fileRequester + ).also { + composeRule.activity.setContentView(it) + activity = composeRule.activity + } } @Test fun `Display the answer text when there is answer`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) - assertThat(widget.answer!!.displayText, equalTo(initialAnswer.displayText)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + createWidget() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() } @Test fun `External file picker should be called when clicking on button`() { - widget!!.binding.exArbitraryFileButton.performClick() + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).performClick() verify(fileRequester).launch(activity, ApplicationConstants.RequestCodes.EX_ARBITRARY_FILE_CHOOSER, formEntryPrompt) } @Test fun `File viewer should be called when clicking on answer`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - widget.binding.answerViewContainer.performClick() - verify(mediaUtils).openFile(activity, widget.answerFile!!, null) + val file = questionMediaManager.addAnswerFile(File.createTempFile("document", ".pdf")) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(file.name)) + .build() + + createWidget() + composeRule.onNodeWithText(file.name).performClick() + verify(mediaUtils).openFile(activity, file, null) } @Test fun `Hide the answer when clear answer is called`() { - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(initialAnswer.displayText)) + .build() - val widget = widget!! + val widget = createWidget() widget.clearAnswer() - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.GONE)) + composeRule.onNodeWithText(initialAnswer.displayText).assertDoesNotExist() } @Test fun `All clickable elements should be disabled when read-only override option is used`() { readOnlyOverride = true - whenever(formEntryPrompt.isReadOnly).thenReturn(false) - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) - - val widget = widget!! - assertThat(widget.binding.exArbitraryFileButton.visibility, equalTo(View.GONE)) - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(false) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() } @Test override fun usingReadOnlyOptionShouldMakeAllClickableElementsDisabled() { - whenever(formEntryPrompt.isReadOnly).thenReturn(true) - whenever(formEntryPrompt.answerText).thenReturn(initialAnswer.displayText) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withReadOnly(true) + .withAnswer(StringData(initialAnswer.displayText)) + .build() + + createWidget() + composeRule.onNodeWithClickLabel(activity.getString(string.launch_app)).assertDoesNotExist() + composeRule.onNodeWithText(initialAnswer.displayText).assertExists() + } - val widget = widget!! - assertThat(widget.binding.exArbitraryFileButton.visibility, equalTo(View.GONE)) - assertThat(widget.binding.answerViewContainer.visibility, equalTo(View.VISIBLE)) + @Test + override fun settingANewAnswerShouldCallDeleteMediaToRemoveTheOldFile() { + val file = questionMediaManager.addAnswerFile(File.createTempFile("document", ".pdf")) + formEntryPrompt = MockFormEntryPromptBuilder(formEntryPrompt) + .withAnswer(StringData(file.name)) + .build() + + val widget = createWidget() + widget.setData(createBinaryData(nextAnswer)) + + assertThat( + questionMediaManager.originalFiles[formEntryPrompt.index.toString()], + equalTo(file.absolutePath) + ) } } diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/WidgetFactoryTest.kt b/collect_app/src/test/java/org/odk/collect/android/widgets/WidgetFactoryTest.kt index 25544d476f7..71f9155ef98 100644 --- a/collect_app/src/test/java/org/odk/collect/android/widgets/WidgetFactoryTest.kt +++ b/collect_app/src/test/java/org/odk/collect/android/widgets/WidgetFactoryTest.kt @@ -24,9 +24,6 @@ import org.odk.collect.android.widgets.items.SelectOneFromMapWidget import org.odk.collect.android.widgets.items.SelectOneImageMapWidget import org.odk.collect.android.widgets.items.SelectOneMinimalWidget import org.odk.collect.android.widgets.items.SelectOneWidget -import org.odk.collect.android.widgets.utilities.QuestionFontSizeUtils -import org.odk.collect.settings.InMemSettingsProvider -import org.odk.collect.settings.keys.ProjectKeys @RunWith(AndroidJUnit4::class) class WidgetFactoryTest { @@ -46,10 +43,7 @@ class WidgetFactoryTest { null, null, null, - mock(), - InMemSettingsProvider().apply { - getUnprotectedSettings().save(ProjectKeys.KEY_FONT_SIZE, QuestionFontSizeUtils.DEFAULT_FONT_SIZE.toString()) - } + mock() ) @Test diff --git a/icons/src/main/res/drawable/ic_baseline_attach_file_white_24.xml b/icons/src/main/res/drawable/ic_baseline_attach_file_white_24.xml deleted file mode 100644 index 206c0548c7a..00000000000 --- a/icons/src/main/res/drawable/ic_baseline_attach_file_white_24.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - -