diff --git a/app/src/main/java/protect/card_locker/ScanActivity.java b/app/src/main/java/protect/card_locker/ScanActivity.java
deleted file mode 100644
index 6625ea113..000000000
--- a/app/src/main/java/protect/card_locker/ScanActivity.java
+++ /dev/null
@@ -1,542 +0,0 @@
-package protect.card_locker;
-
-import static protect.card_locker.BarcodeSelectorActivity.BARCODE_CONTENTS;
-import static protect.card_locker.BarcodeSelectorActivity.BARCODE_FORMAT;
-
-import android.Manifest;
-import android.app.Activity;
-import android.content.ActivityNotFoundException;
-import android.content.Intent;
-import android.content.pm.PackageManager;
-import android.graphics.Color;
-import android.net.Uri;
-import android.os.Bundle;
-import android.provider.Settings;
-import android.text.InputType;
-import android.util.DisplayMetrics;
-import android.util.Log;
-import android.util.TypedValue;
-import android.view.KeyEvent;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.EditText;
-import android.widget.LinearLayout;
-import android.widget.ListAdapter;
-import android.widget.SimpleAdapter;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import androidx.activity.result.ActivityResultLauncher;
-import androidx.activity.result.contract.ActivityResultContracts;
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.widget.Toolbar;
-import androidx.core.content.ContextCompat;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.google.zxing.DecodeHintType;
-import com.google.zxing.ResultPoint;
-import com.google.zxing.client.android.Intents;
-import com.journeyapps.barcodescanner.BarcodeCallback;
-import com.journeyapps.barcodescanner.BarcodeResult;
-import com.journeyapps.barcodescanner.CaptureManager;
-import com.journeyapps.barcodescanner.DecoratedBarcodeView;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-
-import protect.card_locker.databinding.CustomBarcodeScannerBinding;
-import protect.card_locker.databinding.ScanActivityBinding;
-
-/**
- * Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
- *
- * Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
- * originally licensed under Apache 2.0
- */
-public class ScanActivity extends CatimaAppCompatActivity {
- private ScanActivityBinding binding;
- private CustomBarcodeScannerBinding customBarcodeScannerBinding;
- private static final String TAG = "Catima";
-
- private static final int MEDIUM_SCALE_FACTOR_DIP = 460;
- private static final int COMPAT_SCALE_FACTOR_DIP = 320;
-
- private static final int PERMISSION_SCAN_ADD_FROM_IMAGE = 100;
- private static final int PERMISSION_SCAN_ADD_FROM_PDF = 101;
- private static final int PERMISSION_SCAN_ADD_FROM_PKPASS = 102;
-
- private CaptureManager capture;
- private DecoratedBarcodeView barcodeScannerView;
-
- private String cardId;
- private String addGroup;
- private boolean torch = false;
-
- private ActivityResultLauncher manualAddLauncher;
- // can't use the pre-made contract because that launches the file manager for image type instead of gallery
- private ActivityResultLauncher photoPickerLauncher;
- private ActivityResultLauncher pdfPickerLauncher;
- private ActivityResultLauncher pkpassPickerLauncher;
-
- static final String STATE_SCANNER_ACTIVE = "scannerActive";
- private boolean mScannerActive = true;
- private boolean mHasError = false;
-
- private void extractIntentFields(Intent intent) {
- final Bundle b = intent.getExtras();
- cardId = b != null ? b.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID) : null;
- addGroup = b != null ? b.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP) : null;
- Log.d(TAG, "Scan activity: id=" + cardId);
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- binding = ScanActivityBinding.inflate(getLayoutInflater());
- customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner);
- setTitle(R.string.scanCardBarcode);
- setContentView(binding.getRoot());
- Utils.applyWindowInsets(binding.getRoot());
- Toolbar toolbar = binding.toolbar;
- setSupportActionBar(toolbar);
- enableToolbarBackButton();
-
- extractIntentFields(getIntent());
-
- manualAddLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.SELECT_BARCODE_REQUEST, result.getResultCode(), result.getData()));
- photoPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_IMAGE_FILE, result.getResultCode(), result.getData()));
- pdfPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PDF_FILE, result.getResultCode(), result.getData()));
- pkpassPickerLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> handleActivityResult(Utils.BARCODE_IMPORT_FROM_PKPASS_FILE, result.getResultCode(), result.getData()));
- customBarcodeScannerBinding.fabOtherOptions.setOnClickListener(view -> {
- setScannerActive(false);
-
- ArrayList> list = new ArrayList<>();
- String[] texts = new String[]{
- getString(R.string.addWithoutBarcode),
- getString(R.string.addManually),
- getString(R.string.addFromImage),
- getString(R.string.addFromPdfFile),
- getString(R.string.addFromPkpass)
- };
- Object[] icons = new Object[]{
- R.drawable.baseline_block_24,
- R.drawable.ic_edit,
- R.drawable.baseline_image_24,
- R.drawable.baseline_picture_as_pdf_24,
- R.drawable.local_activity_24px
- };
- String[] columns = new String[]{"text", "icon"};
-
- for (int i = 0; i < texts.length; i++) {
- HashMap map = new HashMap<>();
- map.put(columns[0], texts[i]);
- map.put(columns[1], icons[i]);
- list.add(map);
- }
-
- ListAdapter adapter = new SimpleAdapter(
- ScanActivity.this,
- list,
- R.layout.alertdialog_row_with_icon,
- columns,
- new int[]{R.id.textView, R.id.imageView}
- );
-
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
- builder.setTitle(getString(R.string.add_a_card_in_a_different_way));
- builder.setAdapter(
- adapter,
- (dialogInterface, i) -> {
- switch (i) {
- case 0:
- addWithoutBarcode();
- break;
- case 1:
- addManually();
- break;
- case 2:
- addFromImage();
- break;
- case 3:
- addFromPdf();
- break;
- case 4:
- addFromPkPass();
- break;
- default:
- throw new IllegalArgumentException("Unknown 'Add a card in a different way' dialog option");
- }
- }
- );
- builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
- builder.show();
- });
-
- // Configure barcodeScanner
- barcodeScannerView = binding.zxingBarcodeScanner;
- Intent barcodeScannerIntent = new Intent();
- Bundle barcodeScannerIntentBundle = new Bundle();
- barcodeScannerIntentBundle.putBoolean(DecodeHintType.ALSO_INVERTED.name(), Boolean.TRUE);
- barcodeScannerIntent.putExtras(barcodeScannerIntentBundle);
- barcodeScannerView.initializeFromIntent(barcodeScannerIntent);
-
- // Even though we do the actual decoding with the barcodeScannerView
- // CaptureManager needs to be running to show the camera and scanning bar
- capture = new CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError);
- Intent captureIntent = new Intent();
- Bundle captureIntentBundle = new Bundle();
- captureIntentBundle.putBoolean(Intents.Scan.BEEP_ENABLED, false);
- captureIntent.putExtras(captureIntentBundle);
- capture.initializeFromIntent(captureIntent, savedInstanceState);
-
- barcodeScannerView.decodeSingle(new BarcodeCallback() {
- @Override
- public void barcodeResult(BarcodeResult result) {
- LoyaltyCard loyaltyCard = new LoyaltyCard();
- loyaltyCard.setCardId(result.getText());
- loyaltyCard.setBarcodeType(CatimaBarcode.fromBarcode(result.getBarcodeFormat()));
-
- returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
- }
-
- @Override
- public void possibleResultPoints(List resultPoints) {
-
- }
- });
- }
-
- @Override
- protected void onResume() {
- super.onResume();
-
- if (mScannerActive) {
- capture.onResume();
- }
-
- if (!Utils.deviceHasCamera(this)) {
- showCameraError(getString(R.string.noCameraFoundGuideText), false);
- } else if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
- showCameraPermissionMissingText();
- } else {
- hideCameraError();
- }
-
- scaleScreen();
- }
-
- @Override
- protected void onPause() {
- super.onPause();
- capture.onPause();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- capture.onDestroy();
- }
-
- @Override
- protected void onSaveInstanceState(Bundle savedInstanceState) {
- super.onSaveInstanceState(savedInstanceState);
- capture.onSaveInstanceState(savedInstanceState);
-
- savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive);
- }
-
- @Override
- public void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
-
- mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE);
- }
-
- @Override
- public boolean onKeyDown(int keyCode, KeyEvent event) {
- return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event);
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
- getMenuInflater().inflate(R.menu.scan_menu, menu);
- }
-
- barcodeScannerView.setTorchOff();
-
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == android.R.id.home) {
- setResult(Activity.RESULT_CANCELED);
- finish();
- return true;
- } else if (item.getItemId() == R.id.action_toggle_flashlight) {
- if (torch) {
- torch = false;
- barcodeScannerView.setTorchOff();
- item.setTitle(R.string.turn_flashlight_on);
- item.setIcon(R.drawable.ic_flashlight_off_white_24dp);
- } else {
- torch = true;
- barcodeScannerView.setTorchOn();
- item.setTitle(R.string.turn_flashlight_off);
- item.setIcon(R.drawable.ic_flashlight_on_white_24dp);
- }
- }
-
- return super.onOptionsItemSelected(item);
- }
-
- private void setScannerActive(boolean isActive) {
- if (isActive) {
- barcodeScannerView.resume();
- } else {
- barcodeScannerView.pause();
- }
- mScannerActive = isActive;
- }
-
- private void returnResult(ParseResult parseResult) {
- Intent result = new Intent();
- Bundle bundle = parseResult.toLoyaltyCardBundle(ScanActivity.this);
- if (addGroup != null) {
- bundle.putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, addGroup);
- }
- result.putExtras(bundle);
- ScanActivity.this.setResult(RESULT_OK, result);
- finish();
- }
-
- private void handleActivityResult(int requestCode, int resultCode, Intent intent) {
- super.onActivityResult(requestCode, resultCode, intent);
-
- List parseResultList = Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this);
-
- if (parseResultList.isEmpty()) {
- setScannerActive(true);
- return;
- }
-
- Utils.makeUserChooseParseResultFromList(this, parseResultList, new ParseResultListDisambiguatorCallback() {
- @Override
- public void onUserChoseParseResult(ParseResult parseResult) {
- returnResult(parseResult);
- }
-
- @Override
- public void onUserDismissedSelector() {
- setScannerActive(true);
- }
- });
- }
-
- private void addWithoutBarcode() {
- AlertDialog.Builder builder = new MaterialAlertDialogBuilder(this);
-
- builder.setOnCancelListener(dialogInterface -> setScannerActive(true));
-
- // Header
- builder.setTitle(R.string.addWithoutBarcode);
-
- // Layout
- LinearLayout layout = new LinearLayout(this);
- layout.setOrientation(LinearLayout.VERTICAL);
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- );
- int contentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding);
- params.leftMargin = contentPadding;
- params.topMargin = contentPadding / 2;
- params.rightMargin = contentPadding;
-
- // Description
- TextView currentTextview = new TextView(this);
- currentTextview.setText(getString(R.string.enter_card_id));
- currentTextview.setLayoutParams(params);
- layout.addView(currentTextview);
-
- // EditText with spacing
- final EditText input = new EditText(this);
- input.setInputType(InputType.TYPE_CLASS_TEXT);
- input.setLayoutParams(params);
- layout.addView(input);
-
- // Set layout
- builder.setView(layout);
-
- // Buttons
- builder.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
- LoyaltyCard loyaltyCard = new LoyaltyCard();
- loyaltyCard.setCardId(input.getText().toString());
- returnResult(new ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard));
- });
- builder.setNegativeButton(getString(R.string.cancel), (dialog, which) -> dialog.cancel());
- AlertDialog dialog = builder.create();
-
- // Now that the dialog exists, we can bind something that affects the OK button
- input.addTextChangedListener(new SimpleTextWatcher() {
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- if (s.length() == 0) {
- input.setError(getString(R.string.card_id_must_not_be_empty));
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
- } else {
- input.setError(null);
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
- }
- }
- });
-
- dialog.show();
-
- // Disable button (must be done **after** dialog is shown to prevent crash
- dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
- // Set focus on input field
- dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
- input.requestFocus();
- }
-
- public void addManually() {
- MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ScanActivity.this);
- builder.setTitle(R.string.add_manually_warning_title);
- builder.setMessage(R.string.add_manually_warning_message);
- builder.setPositiveButton(R.string.continue_, (dialog, which) -> {
- Intent i = new Intent(getApplicationContext(), BarcodeSelectorActivity.class);
- if (cardId != null) {
- final Bundle b = new Bundle();
- b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId);
- i.putExtras(b);
- }
- manualAddLauncher.launch(i);
- });
- builder.setNegativeButton(R.string.cancel, (dialog, which) -> setScannerActive(true));
- builder.setOnCancelListener(dialog -> setScannerActive(true));
- builder.show();
- }
-
- public void addFromImage() {
- PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE);
- }
-
- public void addFromPdf() {
- PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF);
- }
-
- public void addFromPkPass() {
- PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS);
- }
-
- private void addFromImageOrFileAfterPermission(String mimeType, ActivityResultLauncher launcher, int chooserText, int errorMessage) {
- Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
- photoPickerIntent.setType(mimeType);
- Intent contentIntent = new Intent(Intent.ACTION_GET_CONTENT);
- contentIntent.setType(mimeType);
-
- Intent chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText));
- chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { contentIntent });
- try {
- launcher.launch(chooserIntent);
- } catch (ActivityNotFoundException e) {
- setScannerActive(true);
- Toast.makeText(getApplicationContext(), errorMessage, Toast.LENGTH_LONG).show();
- Log.e(TAG, "No activity found to handle intent", e);
- }
- }
-
- public void onCaptureManagerError(String errorMessage) {
- if (mHasError) {
- // We're already showing an error, ignore this new error
- return;
- }
-
- showCameraError(errorMessage, false);
- }
-
- private void showCameraPermissionMissingText() {
- showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true);
- }
-
- private void showCameraError(String message, boolean setOnClick) {
- customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.setText(message);
-
- setCameraErrorState(true, setOnClick);
- }
-
- private void hideCameraError() {
- setCameraErrorState(false, false);
- }
-
- private void setCameraErrorState(boolean visible, boolean setOnClick) {
- mHasError = visible;
-
- customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(visible && setOnClick ? v -> {
- navigateToSystemPermissionSetting();
- } : null);
- customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(visible ? obtainThemeAttribute(com.google.android.material.R.attr.colorSurface) : Color.TRANSPARENT);
- customBarcodeScannerBinding.cameraErrorLayout.getRoot().setVisibility(visible ? View.VISIBLE : View.GONE);
- }
-
- private void scaleScreen() {
- DisplayMetrics displayMetrics = new DisplayMetrics();
- getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
- int screenHeight = displayMetrics.heightPixels;
- float mediumSizePx = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,MEDIUM_SCALE_FACTOR_DIP,getResources().getDisplayMetrics());
- boolean shouldScaleSmaller = screenHeight < mediumSizePx;
-
- customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
- customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.setVisibility(shouldScaleSmaller ? View.GONE : View.VISIBLE);
- }
-
- private int obtainThemeAttribute(int attribute) {
- TypedValue typedValue = new TypedValue();
- getTheme().resolveAttribute(attribute, typedValue, true);
- return typedValue.data;
- }
-
- private void navigateToSystemPermissionSetting() {
- Intent permissionIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getPackageName(), null));
- permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- startActivity(permissionIntent);
- }
-
- @Override
- public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- super.onRequestPermissionsResult(requestCode, permissions, grantResults);
-
- onMockedRequestPermissionsResult(requestCode, permissions, grantResults);
- }
-
- public void onMockedRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
- boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
-
- if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
- if (granted) {
- hideCameraError();
- } else {
- showCameraPermissionMissingText();
- }
- } else if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE || requestCode == PERMISSION_SCAN_ADD_FROM_PDF || requestCode == PERMISSION_SCAN_ADD_FROM_PKPASS) {
- if (granted) {
- if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
- addFromImageOrFileAfterPermission("image/*", photoPickerLauncher, R.string.addFromImage, R.string.failedLaunchingPhotoPicker);
- } else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
- addFromImageOrFileAfterPermission("application/pdf", pdfPickerLauncher, R.string.addFromPdfFile, R.string.failedLaunchingFileManager);
- } else {
- addFromImageOrFileAfterPermission("application/*", pkpassPickerLauncher, R.string.addFromPkpass, R.string.failedLaunchingFileManager);
- }
- } else {
- setScannerActive(true);
- Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG).show();
- }
- }
- }
-}
diff --git a/app/src/main/java/protect/card_locker/ScanActivity.kt b/app/src/main/java/protect/card_locker/ScanActivity.kt
new file mode 100644
index 000000000..f1dbf6b97
--- /dev/null
+++ b/app/src/main/java/protect/card_locker/ScanActivity.kt
@@ -0,0 +1,599 @@
+package protect.card_locker
+
+
+import android.Manifest
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Color
+import android.net.Uri
+import android.os.Bundle
+import android.provider.Settings
+import android.text.InputType
+import android.util.DisplayMetrics
+import android.util.Log
+import android.util.TypedValue
+import android.view.KeyEvent
+import android.view.Menu
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.WindowManager
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.ListAdapter
+import android.widget.SimpleAdapter
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.appcompat.app.AlertDialog
+import androidx.core.content.ContextCompat
+import androidx.core.widget.doOnTextChanged
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.google.zxing.DecodeHintType
+import com.google.zxing.ResultPoint
+import com.journeyapps.barcodescanner.BarcodeCallback
+import com.journeyapps.barcodescanner.BarcodeResult
+import com.journeyapps.barcodescanner.CaptureManager
+import com.journeyapps.barcodescanner.DecoratedBarcodeView
+import protect.card_locker.databinding.CustomBarcodeScannerBinding
+import protect.card_locker.databinding.ScanActivityBinding
+
+/**
+ * Custom Scannner Activity extending from Activity to display a custom layout form scanner view.
+ *
+ * Based on https://github.com/journeyapps/zxing-android-embedded/blob/0fdfbce9fb3285e985bad9971c5f7c0a7a334e7b/sample/src/main/java/example/zxing/CustomScannerActivity.java
+ * originally licensed under Apache 2.0
+ */
+class ScanActivity : CatimaAppCompatActivity() {
+ private lateinit var binding: ScanActivityBinding
+ private lateinit var customBarcodeScannerBinding: CustomBarcodeScannerBinding
+
+ companion object {
+ private const val TAG = "Catima"
+
+ private const val MEDIUM_SCALE_FACTOR_DIP = 460
+ private const val COMPAT_SCALE_FACTOR_DIP = 320
+
+ private const val PERMISSION_SCAN_ADD_FROM_IMAGE = 100
+ private const val PERMISSION_SCAN_ADD_FROM_PDF = 101
+ private const val PERMISSION_SCAN_ADD_FROM_PKPASS = 102
+
+ private const val STATE_SCANNER_ACTIVE = "scannerActive"
+ }
+
+ private lateinit var capture: CaptureManager
+ private lateinit var barcodeScannerView: DecoratedBarcodeView
+ private var cardId: String? = null
+ private var addGroup: String? = null
+ private var torch = false
+
+ private lateinit var manualAddLauncher: ActivityResultLauncher
+ // can't use the pre-made contract because that launches the file manager for image type instead of gallery
+ private lateinit var photoPickerLauncher: ActivityResultLauncher
+ private lateinit var pdfPickerLauncher: ActivityResultLauncher
+ private lateinit var pkpassPickerLauncher: ActivityResultLauncher
+
+ private var mScannerActive = true
+ private var mHasError = false
+
+ private fun extractIntentFields(intent: Intent) {
+ val b = intent.extras
+ cardId = b?.getString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID)
+ addGroup = b?.getString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP)
+ Log.d(TAG, "Scan activity: id=$cardId")
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ScanActivityBinding.inflate(layoutInflater)
+ customBarcodeScannerBinding = CustomBarcodeScannerBinding.bind(binding.zxingBarcodeScanner)
+ setTitle(R.string.scanCardBarcode)
+ setContentView(binding.root)
+ Utils.applyWindowInsets(binding.root)
+ setSupportActionBar(binding.toolbar)
+ enableToolbarBackButton()
+
+ extractIntentFields(intent)
+
+ manualAddLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ handleActivityResult(
+ Utils.SELECT_BARCODE_REQUEST,
+ result.resultCode,
+ result.data
+ )
+ }
+ photoPickerLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ handleActivityResult(
+ Utils.BARCODE_IMPORT_FROM_IMAGE_FILE,
+ result.resultCode,
+ result.data
+ )
+ }
+ pdfPickerLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ handleActivityResult(
+ Utils.BARCODE_IMPORT_FROM_PDF_FILE,
+ result.resultCode,
+ result.data
+ )
+ }
+ pkpassPickerLauncher =
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
+ handleActivityResult(
+ Utils.BARCODE_IMPORT_FROM_PKPASS_FILE,
+ result.resultCode,
+ result.data
+ )
+ }
+
+ customBarcodeScannerBinding.fabOtherOptions.setOnClickListener {
+ setScannerActive(false)
+
+ val list: ArrayList> = arrayListOf()
+ val texts = arrayOf(
+ getString(R.string.addWithoutBarcode),
+ getString(R.string.addManually),
+ getString(R.string.addFromImage),
+ getString(R.string.addFromPdfFile),
+ getString(R.string.addFromPkpass)
+ )
+ val icons = arrayOf(
+ R.drawable.baseline_block_24,
+ R.drawable.ic_edit,
+ R.drawable.baseline_image_24,
+ R.drawable.baseline_picture_as_pdf_24,
+ R.drawable.local_activity_24px
+ )
+ val columns = arrayOf("text", "icon")
+
+ for (i in 0 until texts.size) {
+ val map: HashMap = hashMapOf()
+ map.put(columns[0], texts[i])
+ map.put(columns[1], icons[i])
+ list.add(map)
+ }
+
+ val adapter: ListAdapter = SimpleAdapter(
+ this,
+ list,
+ R.layout.alertdialog_row_with_icon,
+ columns,
+ intArrayOf(R.id.textView, R.id.imageView)
+ )
+
+ val builder = MaterialAlertDialogBuilder(this).apply {
+ setTitle(getString(R.string.add_a_card_in_a_different_way))
+ setAdapter(adapter) { _, i ->
+ when (i) {
+ 0 -> addWithoutBarcode()
+ 1 -> addManually()
+ 2 -> addFromImage()
+ 3 -> addFromPdf()
+ 4 -> addFromPkPass()
+ else -> throw IllegalArgumentException(
+ "Unknown 'Add a card in a different way' dialog option: $i"
+ )
+ }
+ }
+ setOnCancelListener { _ -> setScannerActive(true) }
+ }
+ builder.show()
+ }
+
+ // Configure barcodeScanner
+ barcodeScannerView = binding.zxingBarcodeScanner
+
+ val barcodeScannerIntent = Intent().apply {
+ val barcodeScannerIntentBundle = Bundle().apply {
+ putBoolean(DecodeHintType.ALSO_INVERTED.name, true)
+ }
+ putExtras(barcodeScannerIntentBundle)
+ }
+ barcodeScannerView.initializeFromIntent(barcodeScannerIntent)
+
+ // Even though we do the actual decoding with the barcodeScannerView
+ // CaptureManager needs to be running to show the camera and scanning bar
+ capture = CatimaCaptureManager(this, barcodeScannerView, this::onCaptureManagerError)
+ val captureIntent = Intent().apply {
+ val captureIntentBundle = Bundle().apply {
+ putBoolean(DecodeHintType.ALSO_INVERTED.name, false)
+ }
+ putExtras(captureIntentBundle)
+ }
+ capture.initializeFromIntent(captureIntent, savedInstanceState)
+
+ barcodeScannerView.decodeSingle(object : BarcodeCallback {
+ override fun barcodeResult(result: BarcodeResult) {
+ val loyaltyCard = LoyaltyCard().apply {
+ setCardId(result.text)
+ setBarcodeType(CatimaBarcode.fromBarcode(result.barcodeFormat))
+ }
+
+ returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
+ }
+
+ override fun possibleResultPoints(resultPoints: List?) {}
+ })
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ if (mScannerActive) {
+ capture.onResume()
+ }
+
+ if (!Utils.deviceHasCamera(this)) {
+ showCameraError(getString(R.string.noCameraFoundGuideText), false)
+ } else if (ContextCompat.checkSelfPermission(
+ this,
+ Manifest.permission.CAMERA
+ ) != PackageManager.PERMISSION_GRANTED
+ ) {
+ showCameraPermissionMissingText()
+ } else {
+ hideCameraError()
+ }
+
+ scaleScreen()
+ }
+
+ override fun onPause() {
+ super.onPause()
+ capture.onPause()
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ capture.onDestroy()
+ }
+
+ override fun onSaveInstanceState(savedInstanceState: Bundle) {
+ super.onSaveInstanceState(savedInstanceState)
+ capture.onSaveInstanceState(savedInstanceState)
+
+ savedInstanceState.putBoolean(STATE_SCANNER_ACTIVE, mScannerActive)
+ }
+
+ override fun onRestoreInstanceState(savedInstanceState: Bundle) {
+ super.onRestoreInstanceState(savedInstanceState)
+
+ mScannerActive = savedInstanceState.getBoolean(STATE_SCANNER_ACTIVE)
+ }
+
+ override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
+ return barcodeScannerView.onKeyDown(keyCode, event) || super.onKeyDown(keyCode, event)
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH)) {
+ menuInflater.inflate(R.menu.scan_menu, menu)
+ }
+
+ barcodeScannerView.setTorchOff()
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == android.R.id.home) {
+ setResult(RESULT_CANCELED)
+ finish()
+ return true
+ } else if (item.itemId == R.id.action_toggle_flashlight) {
+ if (torch) {
+ torch = false
+ barcodeScannerView.setTorchOff()
+ item.setTitle(R.string.turn_flashlight_on)
+ item.setIcon(R.drawable.ic_flashlight_off_white_24dp)
+ } else {
+ torch = true
+ barcodeScannerView.setTorchOn()
+ item.setTitle(R.string.turn_flashlight_off)
+ item.setIcon(R.drawable.ic_flashlight_on_white_24dp)
+ }
+ }
+
+ return super.onOptionsItemSelected(item)
+ }
+
+ private fun setScannerActive(isActive: Boolean) {
+ if (isActive) {
+ barcodeScannerView.resume()
+ } else {
+ barcodeScannerView.pause()
+ }
+ mScannerActive = isActive
+ }
+
+ private fun returnResult(parseResult: ParseResult) {
+ val bundle = parseResult.toLoyaltyCardBundle(this).apply {
+ addGroup?.let { putString(LoyaltyCardEditActivity.BUNDLE_ADDGROUP, it) }
+ }
+ val result = Intent().apply { putExtras(bundle) }
+ this.setResult(RESULT_OK, result)
+ finish()
+ }
+
+ private fun handleActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
+ super.onActivityResult(resultCode, resultCode, intent)
+
+ val parseResultList: List =
+ Utils.parseSetBarcodeActivityResult(requestCode, resultCode, intent, this)
+
+ if (parseResultList.isEmpty()) {
+ setScannerActive(true)
+ return
+ }
+
+
+ Utils.makeUserChooseParseResultFromList(
+ this,
+ parseResultList,
+ object : ParseResultListDisambiguatorCallback {
+ override fun onUserChoseParseResult(parseResult: ParseResult) {
+ returnResult(parseResult)
+ }
+
+ override fun onUserDismissedSelector() {
+ setScannerActive(true)
+ }
+ })
+ }
+
+ private fun addWithoutBarcode() {
+ val builder: AlertDialog.Builder = MaterialAlertDialogBuilder(this).apply {
+ setOnCancelListener { dialogInterface -> setScannerActive(true) }
+ // Header
+ setTitle(R.string.addWithoutBarcode)
+ }
+
+ // Layout
+ val layout = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ }
+ val contentPadding = resources.getDimensionPixelSize(R.dimen.alert_dialog_content_padding)
+ val params = LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ ).apply {
+ leftMargin = contentPadding
+ topMargin = contentPadding / 2
+ rightMargin = contentPadding
+ }
+
+ // Description
+ val currentTextview = TextView(this).apply {
+ text = getString(R.string.enter_card_id)
+ layoutParams = params
+ }
+ layout.addView(currentTextview)
+
+ //EditText with spacing
+ val input = EditText(this).apply {
+ inputType = InputType.TYPE_CLASS_TEXT
+ layoutParams = params
+ }
+ layout.addView(input)
+
+ // Set layout
+ builder.setView(layout).apply {
+
+ setPositiveButton(getString(R.string.ok)) { _, _ ->
+ val loyaltyCard = LoyaltyCard()
+ loyaltyCard.cardId = input.text.toString()
+ returnResult(ParseResult(ParseResultType.BARCODE_ONLY, loyaltyCard))
+ }
+ setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
+ dialog.cancel()
+ }
+ }
+ val dialog: AlertDialog = builder.create()
+
+ // Now that the dialog exists, we can bind something that affects the OK button
+ input.doOnTextChanged { text, _, _, _ ->
+ if (text.isNullOrEmpty()) {
+ input.error = getString(R.string.card_id_must_not_be_empty)
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
+ } else {
+ input.error = null
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
+ }
+ }
+
+ dialog.show()
+
+ // Disable button (must be done **after** dialog is shown to prevent crash
+ dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
+ // Set focus on input field
+ dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
+ input.requestFocus()
+ }
+
+ fun addManually() {
+ val builder = MaterialAlertDialogBuilder(this).apply {
+ setTitle(R.string.add_manually_warning_title)
+ setMessage(R.string.add_manually_warning_message)
+ setPositiveButton(R.string.continue_) { _, _ ->
+ val i = Intent(applicationContext, BarcodeSelectorActivity::class.java)
+ if (cardId != null) {
+ val b = Bundle()
+ b.putString(LoyaltyCard.BUNDLE_LOYALTY_CARD_CARD_ID, cardId)
+ i.putExtras(b)
+ }
+ manualAddLauncher.launch(i)
+ }
+ setNegativeButton(R.string.cancel) { _, _ -> setScannerActive(true) }
+ setOnCancelListener { _ -> setScannerActive(true) }
+ }
+ builder.show()
+ }
+
+ fun addFromImage() {
+ PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_IMAGE)
+ }
+
+ fun addFromPdf() {
+ PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PDF)
+ }
+
+ fun addFromPkPass() {
+ PermissionUtils.requestStorageReadPermission(this, PERMISSION_SCAN_ADD_FROM_PKPASS)
+ }
+
+ private fun addFromImageOrFileAfterPermission(
+ mimeType: String,
+ launcher: ActivityResultLauncher,
+ chooserText: Int,
+ errorMessage: Int
+ ) {
+ val photoPickerIntent = Intent(Intent.ACTION_PICK)
+ photoPickerIntent.type = mimeType
+ val contentIntent = Intent(Intent.ACTION_GET_CONTENT)
+ contentIntent.type = mimeType
+
+ val chooserIntent = Intent.createChooser(photoPickerIntent, getString(chooserText))
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, arrayOf(contentIntent))
+ try {
+ launcher.launch(chooserIntent)
+ } catch (e: ActivityNotFoundException) {
+ setScannerActive(true)
+ Toast.makeText(applicationContext, errorMessage, Toast.LENGTH_LONG).show()
+ Log.e(TAG, "No activity found to handle intent", e)
+ }
+ }
+
+ fun onCaptureManagerError(errorMessage: String) {
+ if (mHasError) {
+ // We're already showing an error, ignore this new error
+ return
+ }
+
+ showCameraError(errorMessage, false)
+ }
+
+ private fun showCameraPermissionMissingText() {
+ showCameraError(getString(R.string.noCameraPermissionDirectToSystemSetting), true)
+ }
+
+ private fun showCameraError(message: String, setOnClick: Boolean) {
+ customBarcodeScannerBinding.cameraErrorLayout.cameraErrorMessage.text = message
+
+ setCameraErrorState(true, setOnClick)
+ }
+
+ private fun hideCameraError() {
+ setCameraErrorState(false, false)
+ }
+
+ private fun setCameraErrorState(visible: Boolean, setOnClick: Boolean) {
+ mHasError = visible
+ customBarcodeScannerBinding.cameraErrorLayout.cameraErrorClickableArea.setOnClickListener(
+ if (visible && setOnClick) { _ -> navigateToSystemPermissionSetting() }
+ else null
+ )
+ customBarcodeScannerBinding.cardInputContainer.setBackgroundColor(
+ if (visible) obtainThemeAttribute(com.google.android.material.R.attr.colorSurface)
+ else Color.TRANSPARENT
+ )
+ customBarcodeScannerBinding.cameraErrorLayout.root.visibility =
+ if (visible) View.VISIBLE else View.GONE
+ }
+
+ private fun scaleScreen() {
+ val displayMetrics = DisplayMetrics()
+ windowManager.defaultDisplay.getMetrics(displayMetrics)
+ val screenHeight: Int = displayMetrics.heightPixels
+ val mediumSizePx: Float = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ MEDIUM_SCALE_FACTOR_DIP.toFloat(),
+ resources.displayMetrics
+ )
+ val shouldScaleSmaller = screenHeight < mediumSizePx
+
+ customBarcodeScannerBinding.cameraErrorLayout.cameraErrorIcon.visibility =
+ if (shouldScaleSmaller) View.GONE else View.VISIBLE
+ customBarcodeScannerBinding.cameraErrorLayout.cameraErrorTitle.visibility =
+ if (shouldScaleSmaller) View.GONE else View.VISIBLE
+ }
+
+ private fun obtainThemeAttribute(attribute: Int): Int {
+ val typedValue = TypedValue()
+ theme.resolveAttribute(attribute, typedValue, true)
+ return typedValue.data
+ }
+
+ private fun navigateToSystemPermissionSetting() {
+ val permissionIntent = Intent(
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.fromParts("package", getPackageName(), null)
+ )
+ permissionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ startActivity(permissionIntent)
+ }
+
+ override fun onRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ onMockedRequestPermissionsResult(requestCode, permissions, grantResults)
+ }
+
+ override fun onMockedRequestPermissionsResult(
+ requestCode: Int,
+ permissions: Array,
+ grantResults: IntArray
+ ) {
+ val granted =
+ grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED
+
+ if (requestCode == CaptureManager.getCameraPermissionReqCode()) {
+ if (granted) {
+ hideCameraError()
+ } else {
+ showCameraPermissionMissingText()
+ }
+ } else if (requestCode in listOf(
+ PERMISSION_SCAN_ADD_FROM_IMAGE,
+ PERMISSION_SCAN_ADD_FROM_PDF,
+ PERMISSION_SCAN_ADD_FROM_PKPASS
+ )
+ ) {
+ if (granted) {
+ if (requestCode == PERMISSION_SCAN_ADD_FROM_IMAGE) {
+ addFromImageOrFileAfterPermission(
+ "image/*",
+ photoPickerLauncher,
+ R.string.addFromImage,
+ R.string.failedLaunchingPhotoPicker
+ )
+ } else if (requestCode == PERMISSION_SCAN_ADD_FROM_PDF) {
+ addFromImageOrFileAfterPermission(
+ "application/pdf",
+ pdfPickerLauncher,
+ R.string.addFromPdfFile,
+ R.string.failedLaunchingFileManager
+ )
+ } else {
+ addFromImageOrFileAfterPermission(
+ "application/*",
+ pkpassPickerLauncher,
+ R.string.addFromPkpass,
+ R.string.failedLaunchingFileManager
+ )
+ }
+ } else {
+ setScannerActive(true)
+ Toast.makeText(this, R.string.storageReadPermissionRequired, Toast.LENGTH_LONG)
+ .show()
+ }
+ }
+ }
+}