diff --git a/app/build.gradle b/app/build.gradle index 4dabccd8..28a2c6cf 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { compileSdk 34 targetSdkVersion 34 multiDexEnabled true - versionCode 42 - versionName "4.6.0" + versionCode 43 + versionName "4.6.1" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' buildFeatures.dataBinding = true vectorDrawables.useSupportLibrary = true diff --git a/app/src/androidTest/java/com/secuso/privacyfriendlycodescanner/qrscanner/DatabaseMigrationInstrumentedTest.java b/app/src/androidTest/java/com/secuso/privacyfriendlycodescanner/qrscanner/DatabaseMigrationInstrumentedTest.java index 4e50f38b..4bf63462 100644 --- a/app/src/androidTest/java/com/secuso/privacyfriendlycodescanner/qrscanner/DatabaseMigrationInstrumentedTest.java +++ b/app/src/androidTest/java/com/secuso/privacyfriendlycodescanner/qrscanner/DatabaseMigrationInstrumentedTest.java @@ -52,7 +52,7 @@ public void useAppContext() throws Exception { // Context of the app under test. Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("com.secuso.privacyfriendlycodescanner.qrscanner", appContext.getPackageName()); + assertEquals("com.secuso.privacyFriendlyCodeScanner", appContext.getPackageName()); } @Before diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index eb9c03df..471da6ee 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -193,4 +194,4 @@ - \ No newline at end of file + diff --git a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/generator/QRGeneratorUtils.java b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/generator/QRGeneratorUtils.java index 55cfc82f..05f2df13 100644 --- a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/generator/QRGeneratorUtils.java +++ b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/generator/QRGeneratorUtils.java @@ -1,10 +1,15 @@ package com.secuso.privacyfriendlycodescanner.qrscanner.generator; +import static android.content.Context.WINDOW_SERVICE; + import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; import android.graphics.Point; import android.media.MediaScannerConnection; import android.net.Uri; @@ -15,14 +20,19 @@ import android.view.Display; import android.view.WindowManager; +import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.FileProvider; import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; import com.google.zxing.WriterException; import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.encoder.ByteMatrix; +import com.google.zxing.qrcode.encoder.Encoder; +import com.google.zxing.qrcode.encoder.QRCode; import java.io.File; import java.io.FileOutputStream; @@ -31,9 +41,9 @@ import java.text.FieldPosition; import java.text.SimpleDateFormat; import java.util.Calendar; +import java.util.HashMap; import java.util.Locale; - -import static android.content.Context.WINDOW_SERVICE; +import java.util.Map; public class QRGeneratorUtils { @@ -85,8 +95,7 @@ public static Uri getCachedUri() { return cache; } - public static Uri createImage(Context context, String qrInputText, Contents.Type qrType, BarcodeFormat barcodeFormat, String errorCorrectionLevel) { - + private static int getDimension(Context context) { //Find screen size WindowManager manager = (WindowManager) context.getSystemService(WINDOW_SERVICE); Display display = manager.getDefaultDisplay(); @@ -94,27 +103,107 @@ public static Uri createImage(Context context, String qrInputText, Contents.Type display.getSize(point); int width = point.x; int height = point.y; - int smallerDimension = width < height ? width : height; - smallerDimension = smallerDimension * 3 / 4; - - //Encode with a QR Code image - QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrInputText, - null, - qrType, - barcodeFormat.toString(), - smallerDimension); - Bitmap bitmap_ = null; - try { - bitmap_ = qrCodeEncoder.encodeAsBitmap(errorCorrectionLevel); - // return bitmap_; + int smallerDimension = Math.min(width, height); + return smallerDimension * 3 / 4; + } - } catch (WriterException e) { - e.printStackTrace(); + public static Uri createImage(Context context, String qrInputText, Contents.Type qrType, BarcodeFormat barcodeFormat, String errorCorrectionLevel, boolean dots) { + int smallerDimension = getDimension(context); + + Bitmap bitmap_ = null; + if (!dots) { + //Encode with a QR Code image + QRCodeEncoder qrCodeEncoder = new QRCodeEncoder(qrInputText, + null, + qrType, + barcodeFormat.toString(), + smallerDimension); + try { + bitmap_ = qrCodeEncoder.encodeAsBitmap(errorCorrectionLevel); + } catch (WriterException e) { + e.printStackTrace(); + } + } else { + final Map hints = new HashMap<>(); + hints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + hints.put(EncodeHintType.MARGIN, 1); + QRCode code; + try { + code = Encoder.encode(qrInputText, ErrorCorrectionLevel.valueOf(errorCorrectionLevel), hints); + } catch (WriterException e) { + throw new RuntimeException(e); + } + bitmap_ = createDotQRCode(code, smallerDimension, smallerDimension, Color.BLACK, Color.WHITE, 1); } return cacheImage(context, bitmap_); } + private static Bitmap createDotQRCode(QRCode code, int width, int height, @ColorInt int color, @ColorInt int backgroundColor, int quietZone) { + Bitmap image = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(image); + canvas.drawColor(backgroundColor); + Paint paint = new Paint(); + paint.setColor(color); + paint.setAntiAlias(true); + paint.setStyle(Paint.Style.FILL); + + + ByteMatrix input = code.getMatrix(); + if (input == null) { + throw new IllegalArgumentException(); + } + + final int QR_WIDTH = input.getWidth() + (quietZone * 2); + final int QR_HEIGHT = input.getHeight() + (quietZone * 2); + final int OUTPUT_WIDTH = Math.max(width, QR_WIDTH); + final int OUTPUT_HEIGHT = Math.max(height, QR_HEIGHT); + + final float SCALE = Math.min((float) OUTPUT_WIDTH / (float) QR_WIDTH, (float) OUTPUT_HEIGHT / (float) QR_HEIGHT); //scale from ByteMatrix to Canvas + final int POSITION_PATTERN_SIZE = 7; //size of the position pattern inside the ByteMatrix + final float POSITION_PATTERN_RADIUS = (SCALE * POSITION_PATTERN_SIZE) / 2f; + final float CIRCLE_RADIUS = (SCALE * 0.35f); + final float PADDING_LEFT = (OUTPUT_WIDTH - (input.getWidth() * SCALE)) / 2.0f + CIRCLE_RADIUS / 2.0f; + final float PADDING_TOP = (OUTPUT_HEIGHT - (input.getHeight() * SCALE)) / 2.0f + CIRCLE_RADIUS / 2.0f; + + for (int y = 0; y < input.getHeight(); y++) { + for (int x = 0; x < input.getWidth(); x++) { + if (input.get(x, y) == 1) { + boolean isInPositionPatternArea = //do not draw anything inside the position pattern regions + x <= POSITION_PATTERN_SIZE && y <= POSITION_PATTERN_SIZE || //top left position pattern + x >= input.getWidth() - POSITION_PATTERN_SIZE && y <= POSITION_PATTERN_SIZE || //top right position pattern + x <= POSITION_PATTERN_SIZE && y >= input.getHeight() - POSITION_PATTERN_SIZE; //bottom left position pattern + if (!isInPositionPatternArea) { + float outputX = PADDING_LEFT + x * SCALE; + float outputY = PADDING_TOP + y * SCALE; + canvas.drawCircle(outputX + CIRCLE_RADIUS, outputY + CIRCLE_RADIUS, CIRCLE_RADIUS, paint); + } + } + } + } + + //draw position patterns + drawPositionPattern(canvas, color, backgroundColor, PADDING_LEFT, PADDING_TOP, POSITION_PATTERN_RADIUS); + drawPositionPattern(canvas, color, backgroundColor, PADDING_LEFT + (input.getWidth() - POSITION_PATTERN_SIZE) * SCALE, PADDING_TOP, POSITION_PATTERN_RADIUS); + drawPositionPattern(canvas, color, backgroundColor, PADDING_LEFT, PADDING_TOP + (input.getHeight() - POSITION_PATTERN_SIZE) * SCALE, POSITION_PATTERN_RADIUS); + + return image; + } + + private static void drawPositionPattern(Canvas canvas, @ColorInt int color, @ColorInt int backgroundColor, float x, float y, float patternRadius) { + final float BACKGROUND_CIRCLE_RADIUS = patternRadius * 5f / 7f; + final float MIDDLE_DOT_RADIUS = patternRadius * 3f / 7f; + + Paint paint = new Paint(); + paint.setColor(color); + Paint bgPaint = new Paint(); + bgPaint.setColor(backgroundColor); + + canvas.drawCircle(x + patternRadius, y + patternRadius, patternRadius, paint); + canvas.drawCircle(x + patternRadius, y + patternRadius, BACKGROUND_CIRCLE_RADIUS, bgPaint); + canvas.drawCircle(x + patternRadius, y + patternRadius, MIDDLE_DOT_RADIUS, paint); + } + public static void saveCachedImageToExternalStorage(Context context) { Bitmap bitmap = null; try { diff --git a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/helpers/Utils.java b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/helpers/Utils.java index 91dcba47..2efa1e7d 100644 --- a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/helpers/Utils.java +++ b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/helpers/Utils.java @@ -79,7 +79,9 @@ public static Bitmap generateCode(String data, BarcodeFormat original_format, in if (!hints.containsKey(ERROR_CORRECTION) && metadata != null && metadata.containsKey(ERROR_CORRECTION_LEVEL) && format.equals(original_format)) { Object ec = metadata.get(ERROR_CORRECTION_LEVEL); if (ec != null) { - hints.put(ERROR_CORRECTION, ec); + String errorCorrection = ec.toString(); + errorCorrection = errorCorrection.replace("%", ""); // Sometimes the error correction value contains a percent sign + hints.put(ERROR_CORRECTION, errorCorrection); } } if (!hints.containsKey(ERROR_CORRECTION) && format != BarcodeFormat.AZTEC && format != BarcodeFormat.PDF_417) { diff --git a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/activities/generator/QrGeneratorDisplayActivity.java b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/activities/generator/QrGeneratorDisplayActivity.java index fec35c9d..b95d3d0f 100644 --- a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/activities/generator/QrGeneratorDisplayActivity.java +++ b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/activities/generator/QrGeneratorDisplayActivity.java @@ -54,14 +54,17 @@ public class QrGeneratorDisplayActivity extends AppCompatActivity { String qrInputText = ""; Contents.Type qrInputType = Contents.Type.UNDEFINED; + private static final String BARCODE_FORMAT_QR_CODE_DOTS = BarcodeFormat.QR_CODE.name() + "_DOTS"; private String[] barcodeFormats = new String[]{ BarcodeFormat.QR_CODE.name(), + BARCODE_FORMAT_QR_CODE_DOTS, BarcodeFormat.AZTEC.name(), BarcodeFormat.DATA_MATRIX.name(), BarcodeFormat.PDF_417.name(), BarcodeFormat.CODE_128.name()}; private Integer[] barcodeFormatIcons = new Integer[]{ R.drawable.ic_baseline_qr_code_24dp, + R.drawable.ic_baseline_qr_code_dots_24dp, R.drawable.ic_aztec_code_24dp, R.drawable.ic_data_matrix_code_24dp, R.drawable.ic_pdf_417_code_24dp, @@ -137,7 +140,7 @@ private void initDropDownMenus() { } private void updateDropDownMenus() { - barcodeFormat = BarcodeFormat.valueOf(barcodeFormatMenu.getText().toString()); + UpdateBarcodeFormatFromMenuValue(); if (barcodeFormat.equals(BarcodeFormat.QR_CODE)) { currentErrorCorrections = errorCorrectionsQR; @@ -151,7 +154,7 @@ private void updateDropDownMenus() { updateErrorCorrectionMenu(); //Update icon ImageView barcodeFormatIcon = findViewById(R.id.iconImageView); - Glide.with(this).load(AppCompatResources.getDrawable(this, barcodeFormatIcons[Arrays.asList(barcodeFormats).indexOf(barcodeFormat.name())])).into(barcodeFormatIcon); + Glide.with(this).load(AppCompatResources.getDrawable(this, barcodeFormatIcons[Arrays.asList(barcodeFormats).indexOf(barcodeFormatMenu.getText().toString())])).into(barcodeFormatIcon); } @@ -176,11 +179,15 @@ private void updateErrorCorrectionMenu() { private void generateAndUpdateImage() { ImageView myImage = findViewById(R.id.resultQRCodeImage); - barcodeFormat = BarcodeFormat.valueOf(barcodeFormatMenu.getText().toString()); + UpdateBarcodeFormatFromMenuValue(); String errorCorrectionLevel = errorCorrectionMenu.getText().toString(); try { Log.d(getClass().getSimpleName(), "Creating image..."); - Glide.with(this).asBitmap().load(QRGeneratorUtils.createImage(this, qrInputText, qrInputType, barcodeFormat, errorCorrectionLevel)).into(new BitmapImageViewTarget(myImage)); + if (barcodeFormatMenu.getText().toString().equals(BARCODE_FORMAT_QR_CODE_DOTS)) { + Glide.with(this).asBitmap().load(QRGeneratorUtils.createImage(this, qrInputText, qrInputType, barcodeFormat, errorCorrectionLevel, true)).into(new BitmapImageViewTarget(myImage)); + } else { + Glide.with(this).asBitmap().load(QRGeneratorUtils.createImage(this, qrInputText, qrInputType, barcodeFormat, errorCorrectionLevel, false)).into(new BitmapImageViewTarget(myImage)); + } } catch (IllegalArgumentException e) { Toast.makeText(this, R.string.code_generation_error, Toast.LENGTH_SHORT).show(); Log.d(getClass().getSimpleName(), "Error during code generation.", e); @@ -263,6 +270,14 @@ protected void onDestroy() { QRGeneratorUtils.purgeCacheFolder(this); } + private void UpdateBarcodeFormatFromMenuValue() { + if (barcodeFormatMenu.getText().toString().equals(BARCODE_FORMAT_QR_CODE_DOTS)) { + barcodeFormat = BarcodeFormat.QR_CODE; + } else { + barcodeFormat = BarcodeFormat.valueOf(barcodeFormatMenu.getText().toString()); + } + } + @Override protected void onResume() { super.onResume(); diff --git a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/ContactResultFragment.java b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/ContactResultFragment.java index 89eec521..8fa58302 100644 --- a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/ContactResultFragment.java +++ b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/ContactResultFragment.java @@ -62,7 +62,7 @@ public void onProceedPressed(Context context) { Intent contact = new Intent(ContactsContract.Intents.Insert.ACTION); contact.setType(ContactsContract.RawContacts.CONTENT_TYPE); - contact.putExtra(ContactsContract.Intents.Insert.NAME, result.getNames() != null ? result.getNames()[0] : null); + contact.putExtra(ContactsContract.Intents.Insert.NAME, result.getNames() != null && result.getNames().length > 0 ? result.getNames()[0] : null); contact.putExtra(ContactsContract.Intents.Insert.COMPANY, result.getOrg()); contact.putExtra(ContactsContract.Intents.Insert.JOB_TITLE, result.getTitle()); contact.putExtra(ContactsContract.Intents.Insert.NOTES, result.getNote()); diff --git a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/URLResultFragment.java b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/URLResultFragment.java index 6c725c27..13ef8236 100644 --- a/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/URLResultFragment.java +++ b/app/src/main/java/com/secuso/privacyfriendlycodescanner/qrscanner/ui/resultfragments/URLResultFragment.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; public class URLResultFragment extends ResultFragment { + private static final String VALID_RFC3986_PROTOCOL_SCHEME = "^[a-zA-Z][a-zA-Z0-9+.-]*:.*$"; URIParsedResult result; @@ -95,23 +96,14 @@ public void onProceedPressed(Context context) { if (!checked) { Toast.makeText(context, R.string.conform_url, Toast.LENGTH_LONG).show(); } else { - String caption; - String qrurl3; - final String lowercase_qrurl = qrurl.toLowerCase(); - if (!lowercase_qrurl.startsWith("http://") && !lowercase_qrurl.startsWith("https://")) { - qrurl3 = "http://" + qrurl; - - Intent url = new Intent(Intent.ACTION_VIEW);/// !!!! - url.setData(Uri.parse(qrurl3)); - caption = getResources().getStringArray(R.array.url_array)[0]; - startActivity(Intent.createChooser(url, caption)); - } else { - Intent url = new Intent(Intent.ACTION_VIEW);/// !!!! - url.setData(Uri.parse(qrurl).normalizeScheme()); - caption = getResources().getStringArray(R.array.url_array)[0]; - startActivity(Intent.createChooser(url, caption)); - + String urlForIntentData = qrurl; + if (!qrurl.matches(VALID_RFC3986_PROTOCOL_SCHEME)) { + urlForIntentData = "http://" + qrurl; } + Intent url = new Intent(Intent.ACTION_VIEW);/// !!!! + url.setData(Uri.parse(urlForIntentData).normalizeScheme()); + String caption = getResources().getStringArray(R.array.url_array)[0]; + startActivity(Intent.createChooser(url, caption)); } } diff --git a/app/src/main/res/drawable/ic_baseline_qr_code_dots_24dp.xml b/app/src/main/res/drawable/ic_baseline_qr_code_dots_24dp.xml new file mode 100644 index 00000000..f2443604 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_qr_code_dots_24dp.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + +