如何在安卓系统中使用快速响应(QR)代码进行数据加密

412 阅读6分钟

在安卓系统中使用快速响应(QR)代码进行数据加密

快速响应(QR)码是一种条形码,或者说是可扫描系列,可以存储一系列数据,包括网络链接、信用卡信息、联系信息,甚至是赠品。二维码被用来以只有机器才能理解的格式对信息进行编码。

在本教程中,我们将学习如何生成QR码,在Android应用程序中存储简单的文本格式数据。

为什么使用快速响应代码?

二维码有以下优点。

  1. 它们不会像传统的条形码那样存在安全风险。
  2. 二维码可容纳大量的数据;超过2500个字符。
  3. 它们最大限度地减少了处理错误,因为它们很难被篡改。
  4. 与传统的条形码不同,二维码以二维方式存储数据(垂直和水平方向)。

前提条件

要跟上这个教程,你需要熟悉。

  • 使用[Kotlin编程语言]创建Android应用程序。
  • Android中的命令式范式(XML)。
  • [Android中的ViewBinding]。
  • [安卓权限]。

创建一个安卓项目

运行Android Studio并建立一个新的Empty Activity 项目,名为QR Code

New Android project

设置该项目

在我们开始编码之前,首先让我们通过以下方式丰富我们的项目。

添加摄像头权限

Android操作系统禁止未经授权使用摄像头。由于我们在扫描二维码时需要它,所以在清单文件中添加以下权限。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.demo.qrcode">

    <uses-permission android:name="android.permission.CAMERA" />

</manifest>

添加所需的依赖性和存储库

ZXing库

ZXing (是 "Zebra Crossing "的缩写)是一个开源的、多格式的一维/二维条码图像处理工具,用Java构建,与其他编程语言兼容。

implementation 'com.google.zxing:core:3.4.0'
implementation 'com.journeyapps:zxing-android-embedded:4.1.0'

布迪耶夫代码扫描器

这个库提供了一个易于使用的API,它有一个现成的带有额外控制按钮的代码扫描仪。

implementation 'com.github.yuriy-budiyev:code-scanner:2.1.0'

JitPack资源库

由于jcenter() 的废弃,Budiyev代码扫描器的开发者建议使用jitpack.io 存储库,因为他们计划迁移到mavenCentral()

// in the top-level build.gradle file
repositories {
    ...

    maven { url 'https://jitpack.io' }
}

视图绑定

以更安全的方式轻松地访问UI中的视图。

android{
    ...

     buildFeatures{
        viewBinding true
    }
}

系统要求

ZXing创建者建议使用Android API-24作为最小的SDK版本。否则,该应用将无法编译。

android {
    ...

    defaultConfig {
        ...

        minSdk 24

    }
}

数据编码/加密

为了说明二维码解码的工作原理,我们要创建一个名为GenerateQR 的活动和其对应的XML布局文件,名为activity_generate_qr 。你也可以重新命名默认的MainActivity.kt 和它的XML文件。

activity_generate_qr.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <com.google.android.material.textfield.TextInputLayout
        android:id="@+id/textInputLayout"
        style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginVertical="50dp"
        app:counterEnabled="true"
        app:counterMaxLength="50"
        app:helperText="Enter text to be encoded"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.google.android.material.textfield.TextInputEditText
            android:id="@+id/inputText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:hint="Type something"
            tools:text="Hello Android" />
    </com.google.android.material.textfield.TextInputLayout>

    <Button
        android:id="@+id/btnGenerateQRCode"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginVertical="16dp"
        android:text="Generate"
        app:layout_constraintEnd_toEndOf="@+id/textInputLayout"
        app:layout_constraintStart_toStartOf="@+id/textInputLayout"
        app:layout_constraintTop_toBottomOf="@+id/textInputLayout" />

    <ImageView
        android:id="@+id/ivOutput"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginVertical="16dp"
        android:adjustViewBounds="true"
        app:layout_constraintBottom_toTopOf="@+id/btnScan"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnGenerateQRCode"
        tools:src="@tools:sample/avatars" />

    <Button
        android:id="@+id/btnScan"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Scan"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

在上面的代码中,我们已经创建了一个文本输入字段,两个按钮和一个ImageView,一旦生成QR码就会显示出来。

预览。

Generate QR Code preview image

GenerateQR.kt

这就是应用生成QR码的逻辑的地方。这个类的目的是将输入(文本)编码成BitMap并在ImageView中显示。

使用ViewBinding设置点击监听器

private var _genQRBinding: ActivityGenerateQrBinding? = null
private val genQRBinding get() = _genQRBinding!!

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    _genQRBinding = ActivityGenerateQrBinding.inflate(layoutInflater)
    setContentView(genQRBinding.root)

    // Handle button clicks
    genQRBinding.btnGenerateQRCode.setOnClickListener {
        val input = genQRBinding.inputText.text!!.toString().trim()
        genQRBinding.ivOutput.setImageBitmap(generateQRCode(input))
    }

    genQRBinding.btnScan.setOnClickListener {
        // This activity will be created later
        startActivity(Intent(this, ScanQR::class.java))
    }
}

生成二维码

private fun generateQRCode(inputText: String?): Bitmap? {
    val writer = MultiFormatWriter()
    var bitmap: Bitmap? = null

    if (!inputText.isNullOrEmpty()) {
        try {
            // init bit matrix
            val matrix = writer.encode(inputText, BarcodeFormat.QR_CODE, 350, 350)
            // init barcode encoder
            val encoder = BarcodeEncoder()
            // generate bitmap
            bitmap = encoder.createBitmap(matrix)
        } catch (e: WriterException) {
            // log error here
            Log.e("GENERATE QR CODE ACTIVITY", e.toString())
        }
    } else {
        genQRBinding.textInputLayout.error = "* required"
    }
    return bitmap
}

解释一下。

在上面的代码中,我们捕获了用户输入的文本,并将其编码为一个正方形的BitMap。

如果输入不是空的,该函数返回BitMap,否则返回null,并在textField中显示错误信息。

如果在编码过程中发生错误,异常会显示在logcat上,以方便调试。

数据矩阵 - 矩阵是一个由黑色和白色单元格组成的二维代码,以正方形排列(也有矩形的)。列和行的数量与代码中存储的数据量成比例增长。

使用ZXing提供的编码器将矩阵转换为BitMap。

运行该应用程序后,你应该看到与此类似的东西。

QR Code example

这就是关于从文本(字符串)生成QR码的全部内容。

数据解码/解密

接下来,让我们看看如何扫描一个已经存在的QR码。这是与编码相反的。

注意:你可以扫描任何QR码,包括由其他系统生成的QR码。解码算法将始终产生相同的结果。

再创建一个名为ScanQR 的活动,以及其相应的名为activity_scan_qr 的XML布局文件。

activity_scan_qr.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.budiyev.android.codescanner.CodeScannerView
        android:id="@+id/codeScannerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:autoFocusButtonColor="@android:color/white"
        app:autoFocusButtonVisible="true"
        app:flashButtonColor="@android:color/white"
        app:flashButtonVisible="true"
        app:frameAspectRatioHeight="1"
        app:frameAspectRatioWidth="1"
        app:frameColor="@android:color/white"
        app:frameCornersRadius="8dp"
        app:frameCornersSize="60dp"
        app:frameSize="0.75"
        app:frameThickness="2dp"
        app:maskColor="#75000000" />

</androidx.constraintlayout.widget.ConstraintLayout>

上面的代码生成了一个代码扫描器,用于在与框架正确对齐时捕获图片。这个代码扫描器提供了两个动作按钮;聚焦按钮和手电筒按钮。

这两个可以通过改变属性中各自的值或使用扫描仪的属性来启用。

预览。

Code Scanner preview

ScanQR.kt文件

与生成QR码时不同,扫描涉及以下讨论的几个阶段。

i) 设置ViewBinding和CodeScanner

膨胀UI并定义代码扫描器变量。

private lateinit var scanBinding: ActivityScanQrBinding
private var codeScanner: CodeScanner? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    scanBinding = ActivityScanQrBinding.inflate(layoutInflater)
    setContentView(scanBinding.root)

    checkPermissions()
}

ii) 检查权限

如前所述,我们需要向系统申请使用摄像头的权限。

private fun checkPermissions() {
    if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CAMERA) ==
        PackageManager.PERMISSION_GRANTED
    ) {
        initiateScan()
    } else {
        // request for Camera permission
        ActivityCompat.requestPermissions(this, arrayOf(android.Manifest.permission.CAMERA), 1)
    }
}

iii) 处理权限回调

这是在权限对话框被驳回后立即调用的。如果用户授予所请求的权限,扫描器就被激活,否则,它就保持不活动。

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == 1) {
        if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
            initiateScan()
        } else {
            Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
        }
    }
}

iv) 初始化一个扫描过程

指派扫描器

这涉及到通过传递上下文和代码扫描器视图来实例化CodeScanner 类。这个对象在扫描过程中一直被使用,每个进程都有一个新的对象,除非应用程序没有被完全杀死。

val scannerView = scanBinding.codeScannerView
codeScanner = CodeScanner(this, scannerView)

默认的扫描器值

以下是代码扫描器采取的默认值。你可以使用点(.)符号来查看可用的选项/值。

codeScanner?.camera = CodeScanner.CAMERA_BACK
codeScanner?.formats = CodeScanner.ALL_FORMATS

codeScanner?.apply {
    isAutoFocusEnabled = true
    isFlashEnabled = false
    autoFocusMode = AutoFocusMode.SAFE
    scanMode = ScanMode.SINGLE
}

解码回调(结果)

以下代码在捕获一个代码后被调用。注意,要成功捕获一个QR码,它必须在扫描器的火焰中对齐。

💡 提示:方向并不重要 😎

codeScanner!!.decodeCallback = DecodeCallback {
    runOnUiThread {
        Snackbar.make(scannerView, "Scan result: ${it.text}", 5000).show()
    }
}

结果显示在一个持续5秒钟的Snackbar中。同样,你也可以用你想要的方式处理结果。

处理解密错误

codeScanner?.errorCallback = ErrorCallback {
    runOnUiThread {
        Toast.makeText(
            this, "Camera initialization error: ${it.message}",
            Toast.LENGTH_LONG
        ).show()
    }
}

只要在解码时发生错误,就会运行这个程序。在大多数情况下,如果QR码无法解码,扫描仪往往会忽略它。这样一来,你就很少得到异常。

当扫描器被敲击时重新启动扫描

scannerView.setOnClickListener {
    codeScanner!!.startPreview()
}

将上面的部分合并为一个函数

private fun initiateScan() {
    val scannerView = scanBinding.codeScannerView
    codeScanner = CodeScanner(this, scannerView)

    // The default values
    codeScanner?.camera = CodeScanner.CAMERA_BACK
    codeScanner?.formats = CodeScanner.ALL_FORMATS

    codeScanner?.apply {
        isAutoFocusEnabled = true
        isFlashEnabled = false
        autoFocusMode = AutoFocusMode.SAFE
        scanMode = ScanMode.SINGLE
    }

    // Decode Callback (Results)
    codeScanner!!.decodeCallback = DecodeCallback {
        runOnUiThread {
            Snackbar.make(scannerView, "Scan result: ${it.text}", 5000).show()
        }
    }
    // Error CallBack
    codeScanner?.errorCallback = ErrorCallback {
        runOnUiThread {
            Toast.makeText(
                this, "Camera initialization error: ${it.message}",
                Toast.LENGTH_LONG
            ).show()
        }
    }

    scannerView.setOnClickListener {
        codeScanner!!.startPreview()
    }
}

处理Android生命周期的回调

// When the app resumes
override fun onResume() {
    super.onResume()
    codeScanner?.startPreview()
}
// just before the app is paused
override fun onPause() {
    codeScanner?.releaseResources()
    super.onPause()
}

运行应用程序

运行应用程序后,导航到ScanQR 活动,你应该期望看到一个与下面类似的二维码扫描仪。

Scan QR Code

Scan QR Code

结语

在本教程中,我们已经介绍了如何在Android应用中使用快速响应(QR)码进行数据编码的基本原理。我们还学习了如何使用CodeScanner 来扫描/解码QR码。

在本教程中获得的知识可以应用于其他适用于QR码的场景。