From 09c13c833eedb7cb9f0b1f74a2b1937f2760dd09 Mon Sep 17 00:00:00 2001 From: Rafael Lins Date: Thu, 9 Nov 2023 23:37:53 -0300 Subject: [PATCH] [4.0.4] Android drawing is different from the rest, Fixes #88 --- build.gradle.kts | 11 ++- .../io/github/g0dkar/qrcode/AboutActivity.kt | 11 +++ .../github/g0dkar/qrcode/NewQRCodeActivity.kt | 85 ++++++++++++++++++ .../io/github/g0dkar/qrcode/QRCodeData.kt | 20 +++++ .../g0dkar/qrcode/QRCodeDetailActivity.kt | 49 ++++++++++ .../g0dkar/qrcode/QRCodeListActivity.kt | 89 +++++++++++++++++++ .../g0dkar/qrcode/extra/QRCodeListAdapter.kt | 63 +++++++++++++ .../qrcode/extra/QRCodeListDatasource.kt | 67 ++++++++++++++ .../drawable-v24/ic_launcher_foreground.xml | 31 +++++++ .../src/main/res/layout/activity_about.xml | 22 +++++ .../src/main/res/layout/activity_main.xml | 40 +++++++++ .../main/res/layout/activity_new_qrcode.xml | 49 ++++++++++ .../res/layout/activity_qrcode_detail.xml | 45 ++++++++++ .../src/main/res/layout/qrcode_list_item.xml | 45 ++++++++++ .../android/src/main/res/menu/menu_main.xml | 11 +++ .../src/main/res/values-pt-rBR/strings.xml | 16 ++++ .../android/src/main/res/xml/backup_rules.xml | 14 +++ .../main/res/xml/data_extraction_rules.xml | 20 +++++ .../main/java/examples/Example01_Shapes.java | 10 +-- .../main/java/examples/Example02_Colors.java | 8 +- .../src/main/java/examples/Example03_SVG.java | 6 +- .../src/main/kotlin/Example01-Shapes.kt | 14 +-- .../src/main/kotlin/Example02-Colors.kt | 8 +- .../kotlin/src/main/kotlin/Example03-Logo.kt | 4 +- .../kotlin/src/main/kotlin/Example04-SVG.kt | 6 +- .../main/kotlin/Example05-BackwardsCompat.kt | 16 ++++ .../kotlin/src/main/kotlin/ProjectLogo.kt | 4 +- gradle.properties | 2 + gradle/libs.versions.toml | 22 ++++- settings.gradle.kts | 2 +- .../qrcode/render/QRCodeGraphics.android.kt | 59 +++++++----- src/commonMain/kotlin/qrcode/QRCode.kt | 12 +-- src/commonMain/kotlin/qrcode/QRCodeBuilder.kt | 2 +- .../kotlin/qrcode/color/ColorType.kt | 3 + .../qrcode/render/QRCodeGraphics.jvm.kt | 1 - .../kotlin/qrcode/raw/QRCodeProcessorTest.kt | 62 +++++++++++++ 36 files changed, 861 insertions(+), 68 deletions(-) create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/AboutActivity.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/NewQRCodeActivity.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeData.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeDetailActivity.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeListActivity.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListAdapter.kt create mode 100644 examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListDatasource.kt create mode 100644 examples/android/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 examples/android/src/main/res/layout/activity_about.xml create mode 100644 examples/android/src/main/res/layout/activity_main.xml create mode 100644 examples/android/src/main/res/layout/activity_new_qrcode.xml create mode 100644 examples/android/src/main/res/layout/activity_qrcode_detail.xml create mode 100644 examples/android/src/main/res/layout/qrcode_list_item.xml create mode 100644 examples/android/src/main/res/menu/menu_main.xml create mode 100644 examples/android/src/main/res/values-pt-rBR/strings.xml create mode 100644 examples/android/src/main/res/xml/backup_rules.xml create mode 100644 examples/android/src/main/res/xml/data_extraction_rules.xml create mode 100644 examples/kotlin/src/main/kotlin/Example05-BackwardsCompat.kt create mode 100644 src/commonMain/kotlin/qrcode/color/ColorType.kt create mode 100644 src/jvmTest/kotlin/qrcode/raw/QRCodeProcessorTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 75a8b4b9..19f23145 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,6 +29,10 @@ plugins { // Docs Plugins alias(libs.plugins.dokka) + + // Here because of the Android Examples + alias(libs.plugins.android.application) apply false + alias(libs.plugins.kotlin.android) apply false } repositories { @@ -41,7 +45,7 @@ val javaVersion = JavaVersion.VERSION_17 val javaVersionNumber = javaVersion.majorVersion.toInt() kotlin { - applyDefaultHierarchyTemplate() + //applyDefaultHierarchyTemplate() jvm { jvmToolchain(javaVersionNumber) @@ -53,7 +57,8 @@ kotlin { } } - androidTarget { + android { +// androidTarget { jvmToolchain(javaVersionNumber) publishLibraryVariants("release") } @@ -69,7 +74,6 @@ kotlin { commonWebpackConfig { mode = PRODUCTION sourceMaps = true -// output?.library = "qrcodeKotlin" } testTask { @@ -77,7 +81,6 @@ kotlin { } binaries.library() -// binaries.executable() generateTypeScriptDefinitions() } } diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/AboutActivity.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/AboutActivity.kt new file mode 100644 index 00000000..ed89f1c4 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/AboutActivity.kt @@ -0,0 +1,11 @@ +package io.github.g0dkar.qrcode + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class AboutActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_about) + } +} diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/NewQRCodeActivity.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/NewQRCodeActivity.kt new file mode 100644 index 00000000..23b6a051 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/NewQRCodeActivity.kt @@ -0,0 +1,85 @@ +package io.github.g0dkar.qrcode + +import android.content.Intent +import android.graphics.Bitmap +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.widget.Button +import android.widget.EditText +import android.widget.ImageView +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.os.postDelayed +import androidx.core.widget.doOnTextChanged +import qrcode.QRCode + +class NewQRCodeActivity : AppCompatActivity() { + private lateinit var qrCodeData: EditText + private lateinit var qrCodeImageView: ImageView + private lateinit var saveButton: Button + private var qrCodeIsValid = false + + private val handler = Handler(Looper.getMainLooper()) + + companion object { + const val QRCODE_DATA = "qrCodeData" + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_new_qrcode) + + qrCodeData = findViewById(R.id.newQRCodeData) + qrCodeImageView = findViewById(R.id.newQRCodePreviewImage) + saveButton = findViewById(R.id.newQRCodeBtn) + + qrCodeData.doOnTextChanged { text, _, _, _ -> + val string = text?.toString()?.trim() + + qrCodeIsValid = if (string.isNullOrBlank()) { + qrCodeImageView.setImageBitmap(null) + + false + } else { + try { + handler.removeCallbacksAndMessages(null) + handler.postDelayed(250) { + val qrCodeBitmap = QRCode(string).render().nativeImage() as Bitmap + qrCodeImageView.setImageBitmap(qrCodeBitmap) + } + + true + } catch (t: Throwable) { + Toast.makeText( + this@NewQRCodeActivity, + R.string.new_qrcode_error_preview, + Toast.LENGTH_LONG, + ).show() + + qrCodeImageView.setImageBitmap(null) + + false + } + } + } + + saveButton.setOnClickListener { + save() + } + } + + private fun save() { + val resultIntent = Intent() + val qrCodeDataText = qrCodeData.text.toString() + + if (qrCodeDataText.isBlank() || !qrCodeIsValid) { + setResult(RESULT_CANCELED, resultIntent) + } else { + resultIntent.putExtra(QRCODE_DATA, qrCodeDataText) + setResult(RESULT_OK, resultIntent) + } + + finish() + } +} diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeData.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeData.kt new file mode 100644 index 00000000..39707660 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeData.kt @@ -0,0 +1,20 @@ +package io.github.g0dkar.qrcode + +import android.content.SharedPreferences +import android.graphics.Bitmap +import qrcode.QRCode +import java.time.OffsetDateTime +import java.time.ZoneOffset + +data class QRCodeData( + val data: String, + val timestamp: OffsetDateTime = OffsetDateTime.now(ZoneOffset.UTC), + val bitmap: Bitmap = QRCode(data).render().nativeImage() as Bitmap, +) : Comparable { + fun persist(sharedPreferencesEditor: SharedPreferences.Editor) { + val key = timestamp.toEpochSecond().toString() + sharedPreferencesEditor.putString(key, data) + } + + override fun compareTo(other: QRCodeData): Int = timestamp.compareTo(other.timestamp) +} diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeDetailActivity.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeDetailActivity.kt new file mode 100644 index 00000000..8037d482 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeDetailActivity.kt @@ -0,0 +1,49 @@ +package io.github.g0dkar.qrcode + +import android.os.Bundle +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import java.time.Instant +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class QRCodeDetailActivity : AppCompatActivity() { + companion object { + const val QRCODE_DATA = "qrCodeData" + const val QRCODE_TIMESTAMP = "qrCodeTimestamp" + } + + private lateinit var qrCodeDetailText: TextView + private lateinit var qrCodeDetailImage: ImageView + private lateinit var qrCodeDetailFab: View + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_qrcode_detail) + + qrCodeDetailText = findViewById(R.id.qrCodeDetailText) + qrCodeDetailImage = findViewById(R.id.qrCodeDetailImage) + qrCodeDetailFab = findViewById(R.id.qrCodeDetailFab) + + setBrightness() + + val qrCodeData = intent.getStringExtra(QRCODE_DATA) ?: "ERROR" + val qrCodeTimestamp = intent.getLongExtra(QRCODE_TIMESTAMP, Instant.now().epochSecond) + val qrCode = QRCodeData(qrCodeData, OffsetDateTime.ofInstant(Instant.ofEpochSecond(qrCodeTimestamp), ZoneOffset.UTC)) + + qrCodeDetailText.text = qrCode.data + qrCodeDetailImage.setImageBitmap(qrCode.bitmap) + } + + private fun setBrightness() { + try { + val layout = window.attributes + layout.screenBrightness = 1.0f + window.attributes = layout + } catch (_: Exception) { + // NOOP + } + } +} \ No newline at end of file diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeListActivity.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeListActivity.kt new file mode 100644 index 00000000..e2e07b47 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/QRCodeListActivity.kt @@ -0,0 +1,89 @@ +package io.github.g0dkar.qrcode + +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.Toast +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import io.github.g0dkar.qrcode.NewQRCodeActivity.Companion.QRCODE_DATA +import io.github.g0dkar.qrcode.extra.QRCodeListAdapter +import io.github.g0dkar.qrcode.extra.QRCodeListDatasource + +class QRCodeListActivity : AppCompatActivity() { + private val listAdapter = QRCodeListAdapter { + val intent = Intent(this@QRCodeListActivity, QRCodeDetailActivity::class.java) + intent.putExtra(QRCodeDetailActivity.QRCODE_DATA, it.data) + intent.putExtra(QRCodeDetailActivity.QRCODE_TIMESTAMP, it.timestamp.toEpochSecond()) + startActivity(intent) + } + + private lateinit var recyclerView: RecyclerView + private lateinit var fab: View + private lateinit var newQRCodeActivity: ActivityResultLauncher + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + fab = findViewById(R.id.fab) + recyclerView = findViewById(R.id.recycler_view) + + QRCodeListDatasource.fromContext(this) + + recyclerView.adapter = listAdapter + + fab.setOnClickListener { + startNewQRCodeActivity() + } + + newQRCodeActivity = registerForActivityResult(StartActivityForResult()) { + if (it.data != null) { + val result = it.resultCode + + if (result == RESULT_OK) { + val qrCodeData = it.data?.getStringExtra(QRCODE_DATA) + + if (qrCodeData != null) { + QRCodeListDatasource.add(qrCodeData) + } else { + Toast.makeText( + this@QRCodeListActivity, + R.string.new_qrcode_error, + Toast.LENGTH_LONG + ).show() + } + } else { + Toast.makeText( + this@QRCodeListActivity, + R.string.new_qrcode_error, + Toast.LENGTH_LONG + ).show() + } + } + } + } + + override fun onResume() { + super.onResume() + + QRCodeListDatasource.liveData.observe(this) { + if (it != null) { + listAdapter.submitList(it) + } + } + } + + override fun onStop() { + super.onStop() + + QRCodeListDatasource.liveData.removeObservers(this) + } + + private fun startNewQRCodeActivity() { + val intent = Intent(this, NewQRCodeActivity::class.java) + newQRCodeActivity.launch(intent) + } +} diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListAdapter.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListAdapter.kt new file mode 100644 index 00000000..1fcf8eb7 --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListAdapter.kt @@ -0,0 +1,63 @@ +package io.github.g0dkar.qrcode.extra + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import io.github.g0dkar.qrcode.QRCodeData +import io.github.g0dkar.qrcode.R + +class QRCodeListAdapter( + private val onClick: (QRCodeData) -> Unit +) : ListAdapter(QRCodeListDiffCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QRCodeListViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.qrcode_list_item, parent, false) + return QRCodeListViewHolder(view, onClick) + } + + override fun onBindViewHolder(holder: QRCodeListViewHolder, position: Int) { + val qrCodeData = getItem(position) + holder.bind(qrCodeData) + } + +} + +class QRCodeListViewHolder( + itemView: View, + val onClick: (QRCodeData) -> Unit +) : RecyclerView.ViewHolder(itemView) { + private val qrCodeListItemTextView: TextView = itemView.findViewById(R.id.qrcode_data) + private val qrCodeListItemImageView: ImageView = itemView.findViewById(R.id.qrcode_image) + private var currentQrCodeData: QRCodeData? = null + + init { + itemView.setOnClickListener { + currentQrCodeData?.let { + onClick(it) + } + } + } + + fun bind(qrCodeData: QRCodeData) { + currentQrCodeData = qrCodeData + + qrCodeListItemTextView.text = qrCodeData.data + qrCodeListItemImageView.setImageBitmap(qrCodeData.bitmap) + } +} + +object QRCodeListDiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: QRCodeData, newItem: QRCodeData): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: QRCodeData, newItem: QRCodeData): Boolean { + return oldItem.data == newItem.data + } +} diff --git a/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListDatasource.kt b/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListDatasource.kt new file mode 100644 index 00000000..30ade87c --- /dev/null +++ b/examples/android/src/main/java/io/github/g0dkar/qrcode/extra/QRCodeListDatasource.kt @@ -0,0 +1,67 @@ +package io.github.g0dkar.qrcode.extra + +import android.content.Context +import android.content.SharedPreferences +import androidx.appcompat.app.AppCompatActivity.MODE_PRIVATE +import androidx.lifecycle.MutableLiveData +import io.github.g0dkar.qrcode.QRCodeData +import java.time.Instant +import java.time.ZoneOffset + +object QRCodeListDatasource { + private const val ERROR = "ERROR" + private const val PREFERENCE_FILE = "qrCodeKotlinAndroidExample" + + private lateinit var sharedPreferences: SharedPreferences + + val liveData: MutableLiveData> = MutableLiveData(mutableListOf()) + + fun fromContext(context: Context) { + val currentList = liveData.value ?: listOf() + sharedPreferences = context.getSharedPreferences(PREFERENCE_FILE, MODE_PRIVATE) + + val newList = currentList.toMutableList().apply { + sharedPreferences.all + .filterValues { it != null && it.toString().isNotBlank() } + .mapTo(this) { (key, value) -> + QRCodeData( + value?.toString() ?: ERROR, + Instant.ofEpochSecond(key.toLong()).atOffset(ZoneOffset.UTC) + ) + } + + if (isEmpty()) { + add(QRCodeData("QRCode Kotlin")) + } + + sortDescending() + } + + liveData.postValue(newList) + } + + fun add(data: String) { + val currentList = liveData.value ?: listOf() + val qrCodeData = QRCodeData(data) + + val editor = sharedPreferences.edit() + qrCodeData.persist(editor) + editor.apply() + + val newList = currentList.toMutableList() + .apply { add(0, qrCodeData) } + liveData.postValue(newList) + } + + fun remove(timestamp: Long) { + val currentList = liveData.value ?: listOf() + val newList = currentList.toMutableList() + + newList.removeIf { it.timestamp.toEpochSecond() == timestamp } + sharedPreferences.edit() + .remove(timestamp.toString()) + .apply() + + liveData.postValue(newList) + } +} diff --git a/examples/android/src/main/res/drawable-v24/ic_launcher_foreground.xml b/examples/android/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..218bbd56 --- /dev/null +++ b/examples/android/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/src/main/res/layout/activity_about.xml b/examples/android/src/main/res/layout/activity_about.xml new file mode 100644 index 00000000..1d341c89 --- /dev/null +++ b/examples/android/src/main/res/layout/activity_about.xml @@ -0,0 +1,22 @@ + + + + + \ No newline at end of file diff --git a/examples/android/src/main/res/layout/activity_main.xml b/examples/android/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..c494031f --- /dev/null +++ b/examples/android/src/main/res/layout/activity_main.xml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/examples/android/src/main/res/layout/activity_new_qrcode.xml b/examples/android/src/main/res/layout/activity_new_qrcode.xml new file mode 100644 index 00000000..c36a432b --- /dev/null +++ b/examples/android/src/main/res/layout/activity_new_qrcode.xml @@ -0,0 +1,49 @@ + + + +