diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java
deleted file mode 100644
index 45ea10e9779..00000000000
--- a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.java
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * Copyright (C) 2009 University of Washington
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
- * in compliance with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software distributed under the License
- * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
- * or implied. See the License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package org.odk.collect.android.widgets;
-
-import static org.odk.collect.android.utilities.Appearances.FRONT;
-import static org.odk.collect.android.utilities.Appearances.hasAppearance;
-
-import android.annotation.SuppressLint;
-import android.app.Activity;
-import android.content.Context;
-import android.util.TypedValue;
-import android.view.View;
-
-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.BarcodeWidgetAnswerBinding;
-import org.odk.collect.android.formentry.questions.QuestionDetails;
-import org.odk.collect.android.utilities.Appearances;
-import org.odk.collect.androidshared.system.CameraUtils;
-import org.odk.collect.androidshared.ui.ToastUtils;
-import org.odk.collect.android.widgets.interfaces.WidgetDataReceiver;
-import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry;
-
-/**
- * Widget that allows user to scan barcodes and add them to the form.
- */
-
-@SuppressLint("ViewConstructor")
-public class BarcodeWidget extends QuestionWidget implements WidgetDataReceiver {
- BarcodeWidgetAnswerBinding binding;
-
- private final WaitingForDataRegistry waitingForDataRegistry;
- private final CameraUtils cameraUtils;
-
- public BarcodeWidget(Context context, QuestionDetails questionDetails, WaitingForDataRegistry waitingForDataRegistry,
- CameraUtils cameraUtils) {
- super(context, questionDetails);
- render();
-
- this.waitingForDataRegistry = waitingForDataRegistry;
- this.cameraUtils = cameraUtils;
- }
-
- @Override
- protected View onCreateAnswerView(Context context, FormEntryPrompt prompt, int answerFontSize) {
- binding = BarcodeWidgetAnswerBinding.inflate(((Activity) context).getLayoutInflater());
-
- if (prompt.isReadOnly()) {
- binding.barcodeButton.setVisibility(GONE);
- } else {
- binding.barcodeButton.setOnClickListener(v -> onButtonClick());
- }
- binding.barcodeAnswerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, answerFontSize);
-
- String answer = prompt.getAnswerText();
- if (answer != null && !answer.isEmpty()) {
- binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_barcode));
- binding.barcodeAnswerText.setText(answer);
- }
-
- updateVisibility();
- return binding.getRoot();
- }
-
- @Override
- public void clearAnswer() {
- binding.barcodeAnswerText.setText(null);
- binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.get_barcode));
- widgetValueChanged();
-
- updateVisibility();
- }
-
- @Override
- public IAnswerData getAnswer() {
- String answer = binding.barcodeAnswerText.getText().toString();
- return answer.isEmpty() ? null : new StringData(answer);
- }
-
- @Override
- public void setData(Object answer) {
- String response = (String) answer;
-
- binding.barcodeAnswerText.setText(stripInvalidCharacters(response));
- binding.barcodeButton.setText(getContext().getString(org.odk.collect.strings.R.string.replace_barcode));
- updateVisibility();
-
- widgetValueChanged();
- }
-
- private void updateVisibility() {
- if (hasAppearance(getFormEntryPrompt(), Appearances.HIDDEN_ANSWER)) {
- binding.barcodeAnswerText.setVisibility(GONE);
- } else {
- binding.barcodeAnswerText.setVisibility(binding.barcodeAnswerText.getText().toString().isBlank() ? GONE : VISIBLE);
- }
- }
-
- // Remove control characters, invisible characters and unused code points.
- private String stripInvalidCharacters(String data) {
- return data == null ? null : data.replaceAll("\\p{C}", "");
- }
-
- @Override
- public void setOnLongClickListener(OnLongClickListener l) {
- binding.barcodeAnswerText.setOnLongClickListener(l);
- binding.barcodeButton.setOnLongClickListener(l);
- }
-
- @Override
- public void cancelLongPress() {
- super.cancelLongPress();
- binding.barcodeButton.cancelLongPress();
- binding.barcodeAnswerText.cancelLongPress();
- }
-
- private void onButtonClick() {
- getPermissionsProvider().requestCameraPermission((Activity) getContext(), () -> {
- waitingForDataRegistry.waitForData(getFormEntryPrompt().getIndex());
-
- IntentIntegrator intent = new IntentIntegrator((Activity) getContext())
- .setCaptureActivity(ScannerWithFlashlightActivity.class);
-
- setCameraIdIfNeeded(getFormEntryPrompt(), intent);
- intent.initiateScan();
- });
- }
-
- private void setCameraIdIfNeeded(FormEntryPrompt prompt, IntentIntegrator intent) {
- if (Appearances.isFrontCameraAppearance(prompt)) {
- if (cameraUtils.isFrontCameraAvailable(getContext())) {
- intent.addExtra(FRONT, true);
- } else {
- ToastUtils.showLongToast(getContext(), org.odk.collect.strings.R.string.error_front_camera_unavailable);
- }
- }
- }
-}
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/BarcodeWidget.kt
new file mode 100644
index 00000000000..b9f69667655
--- /dev/null
+++ b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidget.kt
@@ -0,0 +1,109 @@
+package org.odk.collect.android.widgets
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.view.View
+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.interfaces.WidgetDataReceiver
+import org.odk.collect.android.widgets.utilities.WaitingForDataRegistry
+import org.odk.collect.androidshared.system.CameraUtils
+import org.odk.collect.androidshared.ui.ToastUtils.showLongToast
+import org.odk.collect.permissions.PermissionListener
+import org.odk.collect.strings.R
+
+@SuppressLint("ViewConstructor")
+class BarcodeWidget(
+ context: Context,
+ questionDetails: QuestionDetails,
+ private val waitingForDataRegistry: WaitingForDataRegistry,
+ private val cameraUtils: CameraUtils
+) : QuestionWidget(context, questionDetails), WidgetDataReceiver {
+ lateinit var binding: BarcodeWidgetBinding
+
+ init {
+ render()
+ }
+
+ override fun onCreateAnswerView(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() }
+ }
+ binding.answerView.setHidden(hasAppearance(prompt, Appearances.HIDDEN_ANSWER))
+ binding.answerView.setTextSize(answerFontSize.toFloat())
+
+ val answer = prompt.answerText
+ if (!answer.isNullOrEmpty()) {
+ binding.barcodeButton.text = getContext().getString(R.string.replace_barcode)
+ }
+ binding.answerView.setAnswer(prompt.answerText)
+
+ return binding.root
+ }
+
+ override fun clearAnswer() {
+ binding.answerView.setAnswer(null)
+ binding.barcodeButton.text = context.getString(R.string.get_barcode)
+ widgetValueChanged()
+ }
+
+ override fun getAnswer(): IAnswerData? {
+ val answer = binding.answerView.getAnswer()
+ return if (answer.isEmpty()) null else StringData(answer)
+ }
+
+ override fun setData(answer: Any) {
+ binding.answerView.setAnswer(answer as String)
+ binding.barcodeButton.text = context.getString(R.string.replace_barcode)
+ widgetValueChanged()
+ }
+
+ override fun setOnLongClickListener(l: OnLongClickListener?) {
+ binding.barcodeButton.setOnLongClickListener(l)
+ binding.answerView.setOnLongClickListener(l)
+ }
+
+ override fun cancelLongPress() {
+ super.cancelLongPress()
+ binding.barcodeButton.cancelLongPress()
+ binding.answerView.cancelLongPress()
+ }
+
+ private fun onButtonClick() {
+ getPermissionsProvider().requestCameraPermission(
+ (context as Activity),
+ object : PermissionListener {
+ override fun granted() {
+ waitingForDataRegistry.waitForData(formEntryPrompt.index)
+ val intent = IntentIntegrator(context as Activity)
+ .setCaptureActivity(ScannerWithFlashlightActivity::class.java)
+ setCameraIdIfNeeded(formEntryPrompt, intent)
+ intent.initiateScan()
+ }
+ }
+ )
+ }
+
+ private fun setCameraIdIfNeeded(prompt: FormEntryPrompt, intent: IntentIntegrator) {
+ if (isFrontCameraAppearance(prompt)) {
+ if (cameraUtils.isFrontCameraAvailable(context)) {
+ intent.addExtra(Appearances.FRONT, true)
+ } else {
+ showLongToast(context, R.string.error_front_camera_unavailable)
+ }
+ }
+ }
+}
diff --git a/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt
new file mode 100644
index 00000000000..18f5f10d443
--- /dev/null
+++ b/collect_app/src/main/java/org/odk/collect/android/widgets/BarcodeWidgetAnswer.kt
@@ -0,0 +1,39 @@
+package org.odk.collect.android.widgets
+
+import android.content.Context
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.widget.FrameLayout
+import org.odk.collect.android.databinding.BarcodeWidgetAnswerBinding
+
+class BarcodeWidgetAnswer @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : FrameLayout(context, attrs, defStyle) {
+ private val binding = BarcodeWidgetAnswerBinding.inflate(LayoutInflater.from(context), this, true)
+ private var hidden = false
+
+ fun setAnswer(answer: String?) {
+ binding.answer.text = stripInvalidCharacters(answer)
+ binding.root.visibility = if (hidden || binding.answer.text.isNullOrBlank()) GONE else VISIBLE
+ }
+
+ fun setTextSize(textSize: Float) {
+ binding.answer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize)
+ }
+
+ fun setHidden(hidden: Boolean) {
+ this.hidden = hidden
+ }
+
+ fun getAnswer(): String {
+ return binding.answer.text.toString()
+ }
+
+ // Remove control characters, invisible characters and unused code points.
+ private fun stripInvalidCharacters(data: String?): String? {
+ return data?.replace("\\p{C}".toRegex(), "")
+ }
+}
diff --git a/collect_app/src/main/res/layout/barcode_widget.xml b/collect_app/src/main/res/layout/barcode_widget.xml
new file mode 100644
index 00000000000..913a568ad0d
--- /dev/null
+++ b/collect_app/src/main/res/layout/barcode_widget.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/collect_app/src/main/res/layout/barcode_widget_answer.xml b/collect_app/src/main/res/layout/barcode_widget_answer.xml
index 1fc1bcf1a19..daf129fee74 100644
--- a/collect_app/src/main/res/layout/barcode_widget_answer.xml
+++ b/collect_app/src/main/res/layout/barcode_widget_answer.xml
@@ -1,20 +1,28 @@
-
-
+ app:tint="?colorOnSurface"
+ app:srcCompat="@drawable/ic_baseline_barcode_scanner_white_24"
+ app:layout_constraintTop_toTopOf="@id/answer"
+ app:layout_constraintBottom_toBottomOf="@id/answer"
+ app:layout_constraintStart_toStartOf="parent" />
+ android:id="@+id/answer"
+ style="@style/Widget.Collect.TextView.WidgetAnswer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ app:layout_constraintTop_toTopOf="parent"
+ app:layout_constraintStart_toEndOf="@id/icon"
+ tools:text="123456789" />
-
+
\ No newline at end of file
diff --git a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java
deleted file mode 100644
index 24867ce6d46..00000000000
--- a/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.java
+++ /dev/null
@@ -1,211 +0,0 @@
-package org.odk.collect.android.widgets;
-
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.equalTo;
-import static org.hamcrest.Matchers.nullValue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.mockValueChangedListener;
-import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAnswer;
-import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithAppearance;
-import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.promptWithReadOnly;
-import static org.odk.collect.android.widgets.support.QuestionWidgetHelpers.widgetTestActivity;
-import static org.robolectric.Shadows.shadowOf;
-
-import android.view.View;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.javarosa.core.model.FormIndex;
-import org.javarosa.core.model.data.StringData;
-import org.javarosa.form.api.FormEntryPrompt;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.odk.collect.android.fakes.FakePermissionsProvider;
-import org.odk.collect.android.formentry.questions.QuestionDetails;
-import org.odk.collect.android.listeners.WidgetValueChangedListener;
-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.support.FakeWaitingForDataRegistry;
-import org.odk.collect.androidshared.system.CameraUtils;
-import org.robolectric.shadows.ShadowActivity;
-import org.robolectric.shadows.ShadowToast;
-
-/**
- * @author James Knight
- */
-@RunWith(AndroidJUnit4.class)
-public class BarcodeWidgetTest {
- private final FakeWaitingForDataRegistry waitingForDataRegistry = new FakeWaitingForDataRegistry();
- private final FakePermissionsProvider permissionsProvider = new FakePermissionsProvider();
-
- private WidgetTestActivity widgetTestActivity;
- private ShadowActivity shadowActivity;
- private CameraUtils cameraUtils;
- private View.OnLongClickListener listener;
- private FormIndex formIndex;
-
- @Before
- public void setUp() {
- widgetTestActivity = widgetTestActivity();
- shadowActivity = shadowOf(widgetTestActivity);
-
- cameraUtils = mock(CameraUtils.class);
- listener = mock(View.OnLongClickListener.class);
- formIndex = mock(FormIndex.class);
- permissionsProvider.setPermissionGranted(true);
- }
-
- @Test
- public void usingReaDOnly_shouldHideBarcodeButton() {
- assertThat(createWidget(promptWithReadOnly()).binding.barcodeButton.getVisibility(), equalTo(View.GONE));
- }
-
- @Test
- public void whenPromptHasAnswer_replaceBarcodeButtonIsDisplayed() {
- BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah")));
- assertThat(widget.binding.barcodeButton.getText().toString(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode)));
- }
-
- @Test
- public void whenPromptHasAnswer_answerTextViewShowsCorrectAnswer() {
- BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah")));
- assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo("blah"));
- }
-
- @Test
- public void getAnswer_whenPromptDoesNotHaveAnswer_returnsNull() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- assertThat(widget.getAnswer(), nullValue());
- }
-
- @Test
- public void getAnswer_whenPromptHasAnswer_returnsCorrectAnswer() {
- BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah")));
- assertThat(widget.getAnswer().getDisplayText(), equalTo("blah"));
- }
-
- @Test
- public void clearAnswer_clearsWidgetAnswer() {
- BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah")));
- widget.clearAnswer();
-
- assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo(""));
- assertThat(widget.binding.barcodeButton.getText().toString(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.get_barcode)));
- }
-
- @Test
- public void clearAnswer_callsValueChangeListener() {
- BarcodeWidget widget = createWidget(promptWithAnswer(new StringData("blah")));
- WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);
- widget.clearAnswer();
-
- verify(valueChangedListener).widgetValueChanged(widget);
- }
-
- @Test
- public void setData_updatesWidgetAnswer_afterStrippingInvalidCharacters() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- widget.setData("\ud800blah\b");
- assertThat(widget.binding.barcodeAnswerText.getText().toString(), equalTo("blah"));
- }
-
- @Test
- public void setData_updatesButtonLabel() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- widget.setData("\ud800blah\b");
- assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode)));
- }
-
- @Test
- public void setData_callsValueChangeListener() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- WidgetValueChangedListener valueChangedListener = mockValueChangedListener(widget);
- widget.setData("blah");
-
- verify(valueChangedListener).widgetValueChanged(widget);
- }
-
- @Test
- public void clickingButtonAndAnswerTextViewForLong_callsLongClickListener() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- widget.setOnLongClickListener(listener);
- widget.binding.barcodeButton.performLongClick();
- widget.binding.barcodeAnswerText.performLongClick();
-
- verify(listener).onLongClick(widget.binding.barcodeButton);
- verify(listener).onLongClick(widget.binding.barcodeAnswerText);
- }
-
- @Test
- public void clickingBarcodeButton_whenPermissionIsNotGranted_doesNotLaunchAnyIntent() {
- BarcodeWidget widget = createWidget(promptWithAnswer(null));
- permissionsProvider.setPermissionGranted(false);
- widget.setPermissionsProvider(permissionsProvider);
- widget.binding.barcodeButton.performClick();
-
- assertThat(shadowActivity.getNextStartedActivity(), nullValue());
- assertThat(waitingForDataRegistry.waiting.isEmpty(), equalTo(true));
- }
-
- @Test
- public void clickingBarcodeButton_whenPermissionIsGranted_setsWidgetWaitingForData() {
- FormEntryPrompt prompt = promptWithAnswer(null);
- when(prompt.getIndex()).thenReturn(formIndex);
-
- BarcodeWidget widget = createWidget(prompt);
- widget.setPermissionsProvider(permissionsProvider);
- widget.binding.barcodeButton.performClick();
-
- assertThat(waitingForDataRegistry.waiting.contains(formIndex), equalTo(true));
- }
-
- @Test
- public void clickingBarcodeButton_whenFrontCameraIsNotAvailable_showsFrontCameraNotAvailableToast() {
- when(cameraUtils.isFrontCameraAvailable(any())).thenReturn(false);
- BarcodeWidget widget = createWidget(promptWithAppearance(Appearances.FRONT));
- widget.setPermissionsProvider(permissionsProvider);
- widget.binding.barcodeButton.performClick();
-
- assertThat(ShadowToast.getTextOfLatestToast(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.error_front_camera_unavailable)));
- }
-
- @Test
- public void clickingBarcodeButton_whenFrontCameraIsAvailable_launchesCorrectIntent() {
- when(cameraUtils.isFrontCameraAvailable(any())).thenReturn(true);
- BarcodeWidget widget = createWidget(promptWithAppearance(Appearances.FRONT));
- widget.setPermissionsProvider(permissionsProvider);
- widget.binding.barcodeButton.performClick();
-
- assertThat(shadowActivity.getNextStartedActivity().getBooleanExtra(Appearances.FRONT, false), equalTo(true));
- }
-
- @Test
- public void whenPromptHasHiddenAnswerAppearance_answerIsNotDisplayed() {
- FormEntryPrompt prompt = new MockFormEntryPromptBuilder(promptWithAppearance(Appearances.HIDDEN_ANSWER))
- .withAnswer(new StringData("original contents"))
- .build();
-
- BarcodeWidget widget = createWidget(prompt);
-
- // Check initial value is not shown
- assertThat(widget.binding.barcodeAnswerText.getVisibility(), equalTo(View.GONE));
- assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode)));
- assertThat(widget.getAnswer(), equalTo(new StringData("original contents")));
-
- // Check updates aren't shown
- widget.setData("updated contents");
- assertThat(widget.binding.barcodeAnswerText.getVisibility(), equalTo(View.GONE));
- assertThat(widget.binding.barcodeButton.getText(), equalTo(widgetTestActivity.getString(org.odk.collect.strings.R.string.replace_barcode)));
- assertThat(widget.getAnswer(), equalTo(new StringData("updated contents")));
- }
-
- public BarcodeWidget createWidget(FormEntryPrompt prompt) {
- return new BarcodeWidget(widgetTestActivity, new QuestionDetails(prompt),
- waitingForDataRegistry, cameraUtils);
- }
-}
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
new file mode 100644
index 00000000000..050ea48325f
--- /dev/null
+++ b/collect_app/src/test/java/org/odk/collect/android/widgets/BarcodeWidgetTest.kt
@@ -0,0 +1,241 @@
+package org.odk.collect.android.widgets
+
+import android.view.View
+import android.view.View.OnLongClickListener
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.hamcrest.CoreMatchers.equalTo
+import org.hamcrest.MatcherAssert.assertThat
+import org.javarosa.core.model.FormIndex
+import org.javarosa.core.model.data.StringData
+import org.javarosa.form.api.FormEntryPrompt
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+import org.odk.collect.android.R
+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.utilities.Appearances
+import org.odk.collect.android.widgets.support.FakeWaitingForDataRegistry
+import org.odk.collect.android.widgets.support.QuestionWidgetHelpers
+import org.odk.collect.androidshared.system.CameraUtils
+import org.odk.collect.strings.R.string
+import org.robolectric.Shadows
+import org.robolectric.shadows.ShadowToast
+
+@RunWith(AndroidJUnit4::class)
+class BarcodeWidgetTest {
+ private val waitingForDataRegistry = FakeWaitingForDataRegistry()
+ private val permissionsProvider = FakePermissionsProvider().apply {
+ setPermissionGranted(true)
+ }
+ private val widgetTestActivity = QuestionWidgetHelpers.widgetTestActivity()
+ private val shadowActivity = Shadows.shadowOf(widgetTestActivity)
+ private val cameraUtils = mock()
+ private val listener = mock()
+ private val formIndex = mock()
+
+ @Test
+ fun `The button is hidden in read-only mode`() {
+ assertThat(
+ createWidget(QuestionWidgetHelpers.promptWithReadOnly()).binding.barcodeButton.visibility,
+ equalTo(View.GONE)
+ )
+ }
+
+ @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))
+ )
+ }
+
+ @Test
+ fun `Display the answer if answer is present`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah")))
+ assertThat(
+ widget.binding.answerView.getAnswer(),
+ equalTo("blah")
+ )
+ }
+
+ @Test
+ fun `#getAnswer returns null when there is no answer`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null))
+ assertThat(widget.answer, equalTo(null))
+ }
+
+ @Test
+ fun `#getAnswer returns the answer when there is answer`() {
+ val widget = createWidget(
+ QuestionWidgetHelpers.promptWithAnswer(
+ StringData("blah")
+ )
+ )
+ assertThat(widget.answer!!.displayText, equalTo("blah"))
+ }
+
+ @Test
+ fun `#clearAnswer removes answer and updates button title`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah")))
+ widget.clearAnswer()
+
+ assertThat(
+ widget.binding.answerView.getAnswer(),
+ equalTo("")
+ )
+ assertThat(
+ widget.binding.barcodeButton.text.toString(),
+ equalTo(widgetTestActivity.getString(string.get_barcode))
+ )
+ }
+
+ @Test
+ fun `#clearAnswer calls #valueChangeListener`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(StringData("blah")))
+ val valueChangedListener = QuestionWidgetHelpers.mockValueChangedListener(widget)
+ widget.clearAnswer()
+
+ verify(valueChangedListener).widgetValueChanged(widget)
+ }
+
+ @Test
+ fun `#setData displays sanitized answer`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null))
+ widget.setData("\ud800blah\b")
+ assertThat(
+ widget.binding.answerView.getAnswer(),
+ equalTo("blah")
+ )
+ }
+
+ @Test
+ fun `#setData updates the button title`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null))
+ widget.setData("\ud800blah\b")
+ assertThat(
+ widget.binding.barcodeButton.text,
+ equalTo(widgetTestActivity.getString(string.replace_barcode))
+ )
+ }
+
+ @Test
+ fun `#setData call #valueChangeListener`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null))
+ 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.answerView.performLongClick()
+
+ verify(listener).onLongClick(widget.binding.barcodeButton)
+ verify(listener).onLongClick(widget.binding.answerView)
+ }
+
+ @Test
+ fun `pressing the button with permission not granted does not launch anything`() {
+ val widget = createWidget(QuestionWidgetHelpers.promptWithAnswer(null))
+ permissionsProvider.setPermissionGranted(false)
+ widget.setPermissionsProvider(permissionsProvider)
+ widget.binding.barcodeButton.performClick()
+
+ assertThat(shadowActivity.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)
+
+ val widget = createWidget(prompt)
+ widget.setPermissionsProvider(permissionsProvider)
+ widget.binding.barcodeButton.performClick()
+
+ assertThat(
+ waitingForDataRegistry.waiting.contains(formIndex),
+ equalTo(true)
+ )
+ }
+
+ @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))
+ widget.setPermissionsProvider(permissionsProvider)
+ widget.binding.barcodeButton.performClick()
+
+ assertThat(
+ ShadowToast.getTextOfLatestToast(),
+ equalTo(widgetTestActivity.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))
+ widget.setPermissionsProvider(permissionsProvider)
+ widget.binding.barcodeButton.performClick()
+
+ assertThat(
+ shadowActivity.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()
+
+ val widget = createWidget(prompt)
+
+ // Check initial value is not shown
+ assertThat(
+ widget.binding.answerView.findViewById(R.id.barcode_widget_answer).visibility,
+ equalTo(View.GONE)
+ )
+ assertThat(
+ widget.binding.barcodeButton.text,
+ equalTo(widgetTestActivity.getString(string.replace_barcode))
+ )
+ assertThat(widget.answer, equalTo(StringData("original contents")))
+
+ // Check updates aren't shown
+ widget.setData("updated contents")
+ assertThat(
+ widget.binding.answerView.findViewById(R.id.barcode_widget_answer).visibility,
+ equalTo(View.GONE)
+ )
+ assertThat(
+ widget.binding.barcodeButton.text,
+ equalTo(widgetTestActivity.getString(string.replace_barcode))
+ )
+ assertThat(
+ widget.answer,
+ equalTo(StringData("updated contents"))
+ )
+ }
+
+ private fun createWidget(prompt: FormEntryPrompt?) = BarcodeWidget(
+ widgetTestActivity,
+ QuestionDetails(prompt),
+ waitingForDataRegistry,
+ cameraUtils
+ )
+}