Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion app/src/main/java/app/grapheneos/camera/CamConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1476,7 +1476,13 @@ class CamConfig(private val mActivity: MainActivity) {

if (isQRMode) {
mActivity.qrOverlay.visibility = View.VISIBLE
mActivity.thirdOption.visibility = View.INVISIBLE

mActivity.thirdOption.visibility = View.VISIBLE
mActivity.whiteOptionCircle.visibility = View.INVISIBLE
mActivity.imagePreview.visibility = View.INVISIBLE
mActivity.thirdCircle.visibility = View.INVISIBLE
mActivity.playPreview.visibility = View.INVISIBLE
mActivity.qrImageImport.visibility = View.VISIBLE

if (scanAllCodes) {
mActivity.flipCamIcon.setImageResource(
Expand All @@ -1499,6 +1505,12 @@ class CamConfig(private val mActivity: MainActivity) {
} else {
mActivity.qrOverlay.visibility = View.INVISIBLE
mActivity.thirdOption.visibility = View.VISIBLE

mActivity.qrImageImport.visibility = View.GONE
mActivity.whiteOptionCircle.visibility = View.VISIBLE
mActivity.imagePreview.visibility = View.VISIBLE
mActivity.thirdCircle.visibility = View.VISIBLE

mActivity.flipCamIcon.setImageResource(R.drawable.flip_camera)
mActivity.cancelButtonView.visibility = View.VISIBLE

Expand Down
28 changes: 28 additions & 0 deletions app/src/main/java/app/grapheneos/camera/analyzer/QRAnalyzer.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package app.grapheneos.camera.analyzer

import android.graphics.Bitmap
import android.util.Log
import androidx.camera.core.ImageAnalysis.Analyzer
import androidx.camera.core.ImageProxy
Expand All @@ -9,6 +10,7 @@ import com.google.zxing.BinaryBitmap
import com.google.zxing.DecodeHintType
import com.google.zxing.MultiFormatReader
import com.google.zxing.PlanarYUVLuminanceSource
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.ReaderException
import com.google.zxing.common.HybridBinarizer
import java.util.EnumMap
Expand All @@ -17,6 +19,32 @@ import kotlin.math.roundToInt
class QRAnalyzer(private val mActivity: MainActivity) : Analyzer {
companion object {
private const val TAG = "QRCodeImageAnalyzer"

fun scanStillImage(bitmap: Bitmap, possibleFormats: List<BarcodeFormat>): String? {
val width = bitmap.width
val height = bitmap.height
val pixels = IntArray(width * height)
bitmap.getPixels(pixels, 0, width, 0, 0, width, height)

val hints: MutableMap<DecodeHintType, Any> = EnumMap(DecodeHintType::class.java)
hints[DecodeHintType.POSSIBLE_FORMATS] = possibleFormats
hints[DecodeHintType.TRY_HARDER] = true

val reader = MultiFormatReader().apply { setHints(hints) }

val source = RGBLuminanceSource(width, height, pixels)
decodeOrNull(reader, BinaryBitmap(HybridBinarizer(source)))?.let { return it }
return decodeOrNull(reader, BinaryBitmap(HybridBinarizer(source.invert())))
}

private fun decodeOrNull(reader: MultiFormatReader, binaryBitmap: BinaryBitmap): String? {
reader.reset()
return try {
reader.decodeWithState(binaryBitmap).text
} catch (e: ReaderException) {
null
}
}
}

private var frameCounter = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import android.widget.ProgressBar
import android.widget.RelativeLayout
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.annotation.StringRes
Expand All @@ -70,6 +71,7 @@ import app.grapheneos.camera.CameraMode
import app.grapheneos.camera.ITEM_TYPE_IMAGE
import app.grapheneos.camera.ITEM_TYPE_VIDEO
import app.grapheneos.camera.R
import app.grapheneos.camera.analyzer.QRAnalyzer
import app.grapheneos.camera.capturer.ImageCapturer
import app.grapheneos.camera.capturer.VideoCapturer
import app.grapheneos.camera.capturer.getVideoThumbnail
Expand Down Expand Up @@ -156,6 +158,9 @@ open class MainActivity : AppCompatActivity(),
lateinit var thirdOption: View
lateinit var imagePreview: ShapeableImageView
lateinit var previewLoader: ProgressBar
lateinit var whiteOptionCircle: ImageView
lateinit var playPreview: ImageView
lateinit var qrImageImport: ImageButton
private var isZooming = false

lateinit var zoomBar: ZoomBar
Expand Down Expand Up @@ -315,6 +320,42 @@ open class MainActivity : AppCompatActivity(),
Log.i(TAG, "Selected location: ${data?.encodedPath!!}")
}

private val qrImagePickerLauncher = registerForActivityResult(
ActivityResultContracts.PickVisualMedia()
) { uri: Uri? ->
if (uri != null) {
scanQRFromImage(uri)
}
}

private fun scanQRFromImage(uri: Uri) {
val bitmap = try {
val source = ImageDecoder.createSource(contentResolver, uri)
ImageDecoder.decodeBitmap(source) { decoder, _, _ ->
decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
}
} catch (e: Exception) {
Log.w(TAG, "Unable to decode picked image for QR scan", e)
showMessage(R.string.could_not_read_image)
return
}

val possibleFormats = if (camConfig.scanAllCodes) {
BarcodeFormat.values().asList()
} else {
camConfig.allowedFormats
}

val result = QRAnalyzer.scanStillImage(bitmap, possibleFormats)
bitmap.recycle()

if (result != null) {
onScanResultSuccess(result)
} else {
showMessage(R.string.no_code_found_in_image)
}
}

private fun showAudioPermissionDeniedDialog(onDisableAudio: () -> Unit = {}) {
val builder = MaterialAlertDialogBuilder(this)
.setTitle(R.string.audio_permission_dialog_title)
Expand Down Expand Up @@ -606,6 +647,15 @@ open class MainActivity : AppCompatActivity(),
thirdOption = binding.thirdOption
previewLoader = binding.previewLoading
imagePreview = binding.imagePreview
whiteOptionCircle = binding.whiteOptionCircle
playPreview = binding.playPreview
qrImageImport = binding.qrImageImport
qrImageImport.setOnClickListener {
resetAutoSleep()
qrImagePickerLauncher.launch(
PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)
}
previewView = binding.preview
previewView.scaleType = PreviewView.ScaleType.FIT_START
previewContainer = binding.previewContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,15 @@ import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore.EXTRA_OUTPUT
import android.view.View
import android.widget.ImageView
import app.grapheneos.camera.R

class VideoCaptureActivity : CaptureActivity() {

private lateinit var whiteOptionCircle: ImageView
private lateinit var playPreview: ImageView

private var savedUri: Uri? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

whiteOptionCircle = findViewById(R.id.white_option_circle)
playPreview = findViewById(R.id.play_preview)

captureButton.setImageResource(R.drawable.recording)

captureButton.setOnClickListener OnClickListener@{
Expand Down
12 changes: 12 additions & 0 deletions app/src/main/res/drawable/scan_from_image.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector
android:height="24dp"
android:width="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
xmlns:android="http://schemas.android.com/apk/res/android">

<path
android:fillColor="@android:color/white"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>

</vector>
12 changes: 12 additions & 0 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,18 @@
android:layout_gravity="center"
android:contentDescription="@string/play_preview"/>

<ImageButton
android:id="@+id/qr_image_import"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center"
android:padding="16dp"
android:scaleType="fitCenter"
android:src="@drawable/scan_from_image"
android:background="@drawable/option_circle"
android:visibility="gone"
android:contentDescription="@string/scan_from_image"/>

</FrameLayout>

</LinearLayout>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@
<string name="rec_without_audio">Start recording without audio</string>
<string name="qr_scan_toggle">QR Scan toggle</string>
<string name="qr_scan">QR Code</string>
<string name="scan_from_image">Scan code from an image</string>
<string name="no_code_found_in_image">No code found in the selected image.</string>
<string name="could_not_read_image">Could not read the selected image.</string>
<string name="data_matrix">Data Matrix</string>
<string name="aztec_code">Aztec Code</string>
<string name="more_options">More Options</string>
Expand Down