Skip to content

Commit

Permalink
Merge pull request #9 from StudyProject-NLI/Bene
Browse files Browse the repository at this point in the history
Barcode Scanner MVP
  • Loading branch information
mhaeming authored Feb 11, 2024
2 parents aaa6ea5 + c2b6950 commit 37895b8
Show file tree
Hide file tree
Showing 7 changed files with 275 additions and 9 deletions.
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties
Expand Down
24 changes: 22 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {

android {
namespace 'com.nlinterface'
compileSdk 33
compileSdk 34

defaultConfig {
applicationId "com.nlinterface"
Expand Down Expand Up @@ -36,16 +36,24 @@ android {
buildFeatures {
viewBinding true
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
excludes += "META-INF/DEPENDENCIES"
excludes += "mozilla/public-suffix-list.txt"
}
}
}

dependencies {

implementation 'androidx.core:core-ktx:1.7.0'
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.5.0-alpha04'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.navigation:navigation-fragment-ktx:2.5.3'
implementation 'androidx.navigation:navigation-ui-ktx:2.5.3'
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
Expand All @@ -61,4 +69,16 @@ dependencies {

implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")

//mlkit
implementation 'com.google.mlkit:barcode-scanning:17.2.0'

//it.jsoup
implementation "org.jsoup:jsoup:1.14.3"

//camera
implementation ("androidx.camera:camera-camera2:1.3.1")
implementation("androidx.camera:camera-core:1.3.1")
implementation("androidx.camera:camera-lifecycle:1.3.1")
implementation("androidx.camera:camera-view:1.3.1")

}
7 changes: 6 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

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

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
Expand Down Expand Up @@ -43,7 +46,9 @@
android:name=".activities.PlaceDetailsActivity" />
<activity
android:name=".activities.SettingsActivity"/>

<service
android:name=".viewmodels.ConstantScanning"
android:exported="false" />
</application>

</manifest>
27 changes: 26 additions & 1 deletion app/src/main/java/com/nlinterface/activities/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.lifecycle.ViewModelProvider
import com.nlinterface.R
import com.nlinterface.databinding.ActivityMainBinding
import com.nlinterface.utility.*
import com.nlinterface.viewmodels.ConstantScanning
import com.nlinterface.viewmodels.MainViewModel

/**
Expand Down Expand Up @@ -64,6 +65,14 @@ class MainActivity : AppCompatActivity() {
configureUI()
configureTTS()
configureSTT()

verifyCameraPermissions()
if (checkCallingOrSelfPermission(
Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED) {
val serviceIntent = Intent(this, ConstantScanning()::class.java)
startService(serviceIntent)
}
}

/**
Expand Down Expand Up @@ -285,5 +294,21 @@ class MainActivity : AppCompatActivity() {
viewModel.cancelListening()
}
}


/**
* Request the user to grant camera permissions, if not already granted.
* Will probably not be used further once other camera is included
*/
private fun verifyCameraPermissions() {
if (checkCallingOrSelfPermission(
Manifest.permission.CAMERA
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
STT_PERMISSION_REQUEST_CODE
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package com.nlinterface.viewmodels


import android.app.Service
import android.content.Context
import androidx.camera.core.CameraSelector
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import androidx.camera.lifecycle.ProcessCameraProvider
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
import com.google.mlkit.vision.barcode.BarcodeScanning
import com.google.mlkit.vision.barcode.common.Barcode
import com.google.mlkit.vision.common.InputImage
import androidx.core.content.ContextCompat
import android.content.Intent
import android.os.IBinder
import android.os.Vibrator
import android.util.Log
import androidx.lifecycle.ProcessLifecycleOwner
import org.jsoup.Jsoup
import org.jsoup.nodes.Document

/**
* Classes and functions managing a background Service, +
* which is constantly checking for barcodes
*
* TODO: Add voice command allowing to stop TTS
*/

/**
* The Scanner is used to create a BarcodeScanner object,
* analyze the imageProxy and check for barcodes,
* and defines how to handle them.
*
* @param viewModel: viewModel allows the use of the say function
*/
class Scanner(
private val viewModel: MainViewModel,
private val vibrator: Vibrator
) : ImageAnalysis.Analyzer {

/**
* BarcodeScanner object: Currently only scans EAN 13 barcodes
*/
private val options = BarcodeScannerOptions.Builder()
.setBarcodeFormats(
Barcode.FORMAT_EAN_13
)
.build()
private val barcodeScanner = BarcodeScanning.getClient(options)

/**
* Analysis of the ImageProxy. Converts the camera output into analyzable format.
* Applies the function handleBarcodeResult to the first barcode scanned.
* This is embedded into a Thread to avoid to much calculations on the Main thread.
*
* @param image: uses the camera image without passing an argument directly
*
*/
@ExperimentalGetImage
override fun analyze(image: ImageProxy) {
val mediaImage = image.image
if (mediaImage != null) {
val scannerInput =
InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees)
barcodeScanner.process(scannerInput)
.addOnSuccessListener { barcodes ->
for (barcode in barcodes) {
Log.println(Log.INFO, "Scanner", "Barcode " + barcode.rawValue + " was detected")
val urlAddOn = barcode.rawValue ?: "default"
Thread {
handleBarcodeResult(urlAddOn)
}.start()
break
}
}
.addOnFailureListener { exception ->
exception.printStackTrace()
}
.addOnCompleteListener { image.close() }
}
}

/**
* Function to handle the Barcode Result.
* Initiates a vibration to give feedback to the user, that a barcode was scanned
* It creates an object BrowserSearch to use its function searchUrl
*
* @param urlAddOn: the Barcode as a String to use for web-search
*/
private fun handleBarcodeResult(urlAddOn: String) {
vibrator.vibrate(200)
val eanSearch = BrowserSearch()
Log.println(Log.INFO, "Scraping", "Waiting for Scraping result")
eanSearch.searchUrl(viewModel, urlAddOn)
}
}

/**
* The BrowserSearch class embeds the web-search.
*
*/
class BrowserSearch{

/**
* The function to do the web-search and scraping.
* Url: https://de.openfoodfacts.org/produkt/$barcode
* searches the website with regards to the Barcodes value
* Then scrapes the document for the specified elements and TTS the extracted text.
*
* @param viewModel: viewModel allows the use of the say function
* @param barcode: the barcode as String to add on to the Url
*/
fun searchUrl (viewModel:MainViewModel, barcode: String) {
try {
val searchUrl = "https://de.openfoodfacts.org/produkt/$barcode"
val document: Document = Jsoup.connect(searchUrl).get()
val name = document.select("h1.title-3").first()?.text()
val ingredients =
document.getElementById("panel_ingredients_content")?.text()
val allInfo = name.toString() + ingredients.toString()
viewModel.say(allInfo)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
/**
* The Scanning process handles the camera and initiates the imageAnalysis.
*
*/

class ScanningProcess{

/**
* Function starting the scanning process.
* Selects the camera to be used, initializes an ImageAnalysis object,
* from which to use the scanner method.
* Also binds the camera to a Lifecycle
*
* @param viewModel: passed on to allow using the say function
* @param context: context for the cameraProvider and the imageAnalyzes
*
* TODO: Change selector to other camera
*/
fun activateScanning(viewModel: MainViewModel, vibrator: Vibrator, context: Context) {

val selector = CameraSelector.Builder()
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
.build()
val imageAnalysis = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
imageAnalysis.setAnalyzer(
ContextCompat.getMainExecutor(context),
Scanner(viewModel, vibrator)
)
val cameraProvider = ProcessCameraProvider.getInstance(context).get()
try {
cameraProvider.bindToLifecycle(
ProcessLifecycleOwner.get(),
selector,
imageAnalysis
)
Log.println(Log.INFO, "Camera", "Camera binding successful")

} catch (e: Exception) {
e.printStackTrace().toString()
}
}
}

/**
* ConstantScanning is a Service,
* meaning it is allowed to run in the background parallel to the other activity.
* Also it runs without an UI, which is wanted in the case of the Scanner
*/

class ConstantScanning: Service() {

/**
* This method is required by the Service architecture,
* but not needed because the scanning should be done constantly.
* Therefore it just return null
*/
override fun onBind(intent: Intent?): IBinder? {
return null
}

/**
* Function that manages the Service, once initialized.
* It creates a viewModel object to be passed on, so the say method can be used later on.
* It also creates a Vibrator and Scanner object and starts the Scanning process.
* Returns Start-Sticky to ensure it will try to start again,
* if it is destroyed for whatever reason
*
* @param intent
* @param flags
* @param startId
*/

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {



val application = application
val viewModel = MainViewModel(application)
viewModel.initTTS()
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val scanner = ScanningProcess()
scanner.activateScanning(viewModel, vibrator, this)
Log.println(Log.INFO, "Scanner","Barcode Scanning Service is Active")

return START_STICKY
}

}
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ buildscript {
}

plugins {
id 'com.android.application' version '8.1.2' apply false
id 'com.android.library' version '8.1.2' apply false
id 'com.android.application' version '8.2.0' apply false
id 'com.android.library' version '8.2.0' apply false
id 'org.jetbrains.kotlin.android' version '1.7.10' apply false
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Thu May 25 17:04:02 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

0 comments on commit 37895b8

Please sign in to comment.