Skip to content

Commit 356f02e

Browse files
committed
Integrate barcode scanner
2 parents 514e809 + 37895b8 commit 356f02e

6 files changed

Lines changed: 264 additions & 7 deletions

File tree

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
# Gradle files
2-
.gradle/
3-
build/
42

53
# Local configuration file (sdk path, etc)
64
local.properties

app/build.gradle

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ android {
3535
buildFeatures {
3636
viewBinding true
3737
}
38+
packaging {
39+
resources {
40+
excludes += "/META-INF/{AL2.0,LGPL2.1}"
41+
excludes += "META-INF/DEPENDENCIES"
42+
excludes += "mozilla/public-suffix-list.txt"
43+
}
44+
}
3845
}
3946

4047
dependencies {
@@ -45,6 +52,7 @@ dependencies {
4552
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
4653
implementation 'androidx.navigation:navigation-fragment-ktx:2.7.7'
4754
implementation 'androidx.navigation:navigation-ui-ktx:2.7.7'
55+
implementation 'androidx.lifecycle:lifecycle-process:2.7.0'
4856
testImplementation 'junit:junit:4.13.2'
4957
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
5058
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
@@ -75,4 +83,9 @@ dependencies {
7583

7684
// Camera UI
7785
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
78-
}
86+
//mlkit
87+
implementation 'com.google.mlkit:barcode-scanning:17.2.0'
88+
89+
//it.jsoup
90+
implementation "org.jsoup:jsoup:1.14.3"
91+
}

app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
88
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
9+
<uses-permission android:name="android.permission.VIBRATE" />
910
<uses-feature
1011
android:name="android.hardware.camera"
1112
android:required="false" />
@@ -48,7 +49,9 @@
4849
android:name=".activities.SettingsActivity"/>
4950
<activity
5051
android:name=".activities.ClassificationActivity"/>
51-
52+
<service
53+
android:name=".viewmodels.ConstantScanning"
54+
android:exported="false" />
5255
</application>
5356

54-
</manifest>
57+
</manifest>

app/src/main/java/com/nlinterface/activities/MainActivity.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.lifecycle.ViewModelProvider
1717
import com.nlinterface.R
1818
import com.nlinterface.databinding.ActivityMainBinding
1919
import com.nlinterface.utility.*
20+
import com.nlinterface.viewmodels.ConstantScanning
2021
import com.nlinterface.viewmodels.MainViewModel
2122

2223
/**
@@ -64,6 +65,14 @@ class MainActivity : AppCompatActivity() {
6465
configureUI()
6566
configureTTS()
6667
configureSTT()
68+
69+
verifyCameraPermissions()
70+
if (checkCallingOrSelfPermission(
71+
Manifest.permission.CAMERA
72+
) == PackageManager.PERMISSION_GRANTED) {
73+
val serviceIntent = Intent(this, ConstantScanning()::class.java)
74+
startService(serviceIntent)
75+
}
6776
}
6877

6978
/**
@@ -291,5 +300,21 @@ class MainActivity : AppCompatActivity() {
291300
viewModel.cancelListening()
292301
}
293302
}
294-
303+
304+
/**
305+
* Request the user to grant camera permissions, if not already granted.
306+
* Will probably not be used further once other camera is included
307+
*/
308+
private fun verifyCameraPermissions() {
309+
if (checkCallingOrSelfPermission(
310+
Manifest.permission.CAMERA
311+
) != PackageManager.PERMISSION_GRANTED
312+
) {
313+
ActivityCompat.requestPermissions(
314+
this,
315+
arrayOf(Manifest.permission.CAMERA),
316+
STT_PERMISSION_REQUEST_CODE
317+
)
318+
}
319+
}
295320
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package com.nlinterface.viewmodels
2+
3+
4+
import android.app.Service
5+
import android.content.Context
6+
import androidx.camera.core.CameraSelector
7+
import androidx.camera.core.ExperimentalGetImage
8+
import androidx.camera.core.ImageAnalysis
9+
import androidx.camera.core.ImageProxy
10+
import androidx.camera.lifecycle.ProcessCameraProvider
11+
import com.google.mlkit.vision.barcode.BarcodeScannerOptions
12+
import com.google.mlkit.vision.barcode.BarcodeScanning
13+
import com.google.mlkit.vision.barcode.common.Barcode
14+
import com.google.mlkit.vision.common.InputImage
15+
import androidx.core.content.ContextCompat
16+
import android.content.Intent
17+
import android.os.IBinder
18+
import android.os.Vibrator
19+
import android.util.Log
20+
import androidx.lifecycle.ProcessLifecycleOwner
21+
import org.jsoup.Jsoup
22+
import org.jsoup.nodes.Document
23+
24+
/**
25+
* Classes and functions managing a background Service, +
26+
* which is constantly checking for barcodes
27+
*
28+
* TODO: Add voice command allowing to stop TTS
29+
*/
30+
31+
/**
32+
* The Scanner is used to create a BarcodeScanner object,
33+
* analyze the imageProxy and check for barcodes,
34+
* and defines how to handle them.
35+
*
36+
* @param viewModel: viewModel allows the use of the say function
37+
*/
38+
class Scanner(
39+
private val viewModel: MainViewModel,
40+
private val vibrator: Vibrator
41+
) : ImageAnalysis.Analyzer {
42+
43+
/**
44+
* BarcodeScanner object: Currently only scans EAN 13 barcodes
45+
*/
46+
private val options = BarcodeScannerOptions.Builder()
47+
.setBarcodeFormats(
48+
Barcode.FORMAT_EAN_13
49+
)
50+
.build()
51+
private val barcodeScanner = BarcodeScanning.getClient(options)
52+
53+
/**
54+
* Analysis of the ImageProxy. Converts the camera output into analyzable format.
55+
* Applies the function handleBarcodeResult to the first barcode scanned.
56+
* This is embedded into a Thread to avoid to much calculations on the Main thread.
57+
*
58+
* @param image: uses the camera image without passing an argument directly
59+
*
60+
*/
61+
@ExperimentalGetImage
62+
override fun analyze(image: ImageProxy) {
63+
val mediaImage = image.image
64+
if (mediaImage != null) {
65+
val scannerInput =
66+
InputImage.fromMediaImage(mediaImage, image.imageInfo.rotationDegrees)
67+
barcodeScanner.process(scannerInput)
68+
.addOnSuccessListener { barcodes ->
69+
for (barcode in barcodes) {
70+
Log.println(Log.INFO, "Scanner", "Barcode " + barcode.rawValue + " was detected")
71+
val urlAddOn = barcode.rawValue ?: "default"
72+
Thread {
73+
handleBarcodeResult(urlAddOn)
74+
}.start()
75+
break
76+
}
77+
}
78+
.addOnFailureListener { exception ->
79+
exception.printStackTrace()
80+
}
81+
.addOnCompleteListener { image.close() }
82+
}
83+
}
84+
85+
/**
86+
* Function to handle the Barcode Result.
87+
* Initiates a vibration to give feedback to the user, that a barcode was scanned
88+
* It creates an object BrowserSearch to use its function searchUrl
89+
*
90+
* @param urlAddOn: the Barcode as a String to use for web-search
91+
*/
92+
private fun handleBarcodeResult(urlAddOn: String) {
93+
vibrator.vibrate(200)
94+
val eanSearch = BrowserSearch()
95+
Log.println(Log.INFO, "Scraping", "Waiting for Scraping result")
96+
eanSearch.searchUrl(viewModel, urlAddOn)
97+
}
98+
}
99+
100+
/**
101+
* The BrowserSearch class embeds the web-search.
102+
*
103+
*/
104+
class BrowserSearch{
105+
106+
/**
107+
* The function to do the web-search and scraping.
108+
* Url: https://de.openfoodfacts.org/produkt/$barcode
109+
* searches the website with regards to the Barcodes value
110+
* Then scrapes the document for the specified elements and TTS the extracted text.
111+
*
112+
* @param viewModel: viewModel allows the use of the say function
113+
* @param barcode: the barcode as String to add on to the Url
114+
*/
115+
fun searchUrl (viewModel:MainViewModel, barcode: String) {
116+
try {
117+
val searchUrl = "https://de.openfoodfacts.org/produkt/$barcode"
118+
val document: Document = Jsoup.connect(searchUrl).get()
119+
val name = document.select("h1.title-3").first()?.text()
120+
val ingredients =
121+
document.getElementById("panel_ingredients_content")?.text()
122+
val allInfo = name.toString() + ingredients.toString()
123+
viewModel.say(allInfo)
124+
} catch (e: Exception) {
125+
e.printStackTrace()
126+
}
127+
}
128+
}
129+
/**
130+
* The Scanning process handles the camera and initiates the imageAnalysis.
131+
*
132+
*/
133+
134+
class ScanningProcess{
135+
136+
/**
137+
* Function starting the scanning process.
138+
* Selects the camera to be used, initializes an ImageAnalysis object,
139+
* from which to use the scanner method.
140+
* Also binds the camera to a Lifecycle
141+
*
142+
* @param viewModel: passed on to allow using the say function
143+
* @param context: context for the cameraProvider and the imageAnalyzes
144+
*
145+
* TODO: Change selector to other camera
146+
*/
147+
fun activateScanning(viewModel: MainViewModel, vibrator: Vibrator, context: Context) {
148+
149+
val selector = CameraSelector.Builder()
150+
.requireLensFacing(CameraSelector.LENS_FACING_BACK)
151+
.build()
152+
val imageAnalysis = ImageAnalysis.Builder()
153+
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
154+
.build()
155+
imageAnalysis.setAnalyzer(
156+
ContextCompat.getMainExecutor(context),
157+
Scanner(viewModel, vibrator)
158+
)
159+
val cameraProvider = ProcessCameraProvider.getInstance(context).get()
160+
try {
161+
cameraProvider.bindToLifecycle(
162+
ProcessLifecycleOwner.get(),
163+
selector,
164+
imageAnalysis
165+
)
166+
Log.println(Log.INFO, "Camera", "Camera binding successful")
167+
168+
} catch (e: Exception) {
169+
e.printStackTrace().toString()
170+
}
171+
}
172+
}
173+
174+
/**
175+
* ConstantScanning is a Service,
176+
* meaning it is allowed to run in the background parallel to the other activity.
177+
* Also it runs without an UI, which is wanted in the case of the Scanner
178+
*/
179+
180+
class ConstantScanning: Service() {
181+
182+
/**
183+
* This method is required by the Service architecture,
184+
* but not needed because the scanning should be done constantly.
185+
* Therefore it just return null
186+
*/
187+
override fun onBind(intent: Intent?): IBinder? {
188+
return null
189+
}
190+
191+
/**
192+
* Function that manages the Service, once initialized.
193+
* It creates a viewModel object to be passed on, so the say method can be used later on.
194+
* It also creates a Vibrator and Scanner object and starts the Scanning process.
195+
* Returns Start-Sticky to ensure it will try to start again,
196+
* if it is destroyed for whatever reason
197+
*
198+
* @param intent
199+
* @param flags
200+
* @param startId
201+
*/
202+
203+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
204+
205+
206+
207+
val application = application
208+
val viewModel = MainViewModel(application)
209+
viewModel.initTTS()
210+
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
211+
val scanner = ScanningProcess()
212+
scanner.activateScanning(viewModel, vibrator, this)
213+
Log.println(Log.INFO, "Scanner","Barcode Scanning Service is Active")
214+
215+
return START_STICKY
216+
}
217+
218+
}

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ plugins {
99
id 'com.android.application' version '8.2.2' apply false
1010
id 'com.android.library' version '8.2.2' apply false
1111
id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
12-
}
12+
}

0 commit comments

Comments
 (0)