diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1869807ab350a6f0cc2732fc277042c1cb3d3e2d..58f2ba955bc75a4d217f1c74c00b0b8418d30fa2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -56,9 +56,10 @@ dependencies { //Pie Chart implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") - //Excel - implementation("org.apache.poi:poi:5.2.5") - implementation("org.apache.poi:poi-ooxml:5.2.5") + //Apache POI for saving file into xls/xlsx + val apachePoiVersion = "5.2.5" + implementation("org.apache.poi:poi:$apachePoiVersion") + implementation("org.apache.poi:poi-ooxml:$apachePoiVersion") //Test tools testImplementation("junit:junit:4.13.2") @@ -66,9 +67,23 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") //Room - implementation("androidx.room:room-runtime:2.6.1") - ksp("androidx.room:room-compiler:2.6.1") - implementation("androidx.room:room-ktx:2.6.1") - implementation("com.squareup.retrofit2:retrofit:2.9.0") - implementation("com.squareup.retrofit2:converter-gson:2.9.0") + val roomVersion = "2.6.1" + implementation("androidx.room:room-runtime:$roomVersion") + ksp("androidx.room:room-compiler:$roomVersion") + implementation("androidx.room:room-ktx:$roomVersion") + + //Retrofit + val retrofitVersion = "2.9.0" + implementation("com.squareup.retrofit2:retrofit:$retrofitVersion") + implementation("com.squareup.retrofit2:converter-gson:$retrofitVersion") + + //CameraX for scan + val cameraXVersion = "1.3.2" + implementation ("androidx.camera:camera-core:${cameraXVersion}") + implementation ("androidx.camera:camera-camera2:${cameraXVersion}") + implementation ("androidx.camera:camera-lifecycle:${cameraXVersion}") +// implementation ("androidx.camera:camera-video:${cameraXVersion}") + + implementation ("androidx.camera:camera-view:${cameraXVersion}") + implementation ("androidx.camera:camera-extensions:${cameraXVersion}") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a9a41b289f2a17d94d625bf58c1bf5895e32fd95..9ec7dc746c70df12990b96a8b3272fbef8c4636b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,10 @@ xmlns:tools="http://schemas.android.com/tools"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-feature android:name="android.hardware.camera.any" /> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" + android:maxSdkVersion="28" /> <application android:name=".BondomanApp" android:allowBackup="true" diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt index 5f0ca01c0d6607af9a1a220a0bc7eba78397187e..2e925835011df920ccd93609172932323c6f2452 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanFragment.kt @@ -1,13 +1,33 @@ package pbd.tubes.exe_android.ui.scan +import android.content.ContentValues +import android.content.pm.PackageManager import android.os.Bundle +import android.provider.MediaStore +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageAnalysis +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.ImageProxy +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import pbd.tubes.exe_android.databinding.FragmentScanBinding +import java.nio.ByteBuffer +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +typealias LumaListener = (luma: Double) -> Unit class ScanFragment : Fragment() { @@ -16,26 +36,181 @@ class ScanFragment : Fragment() { // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! + private val viewModel: ScanViewModel by viewModels() + + private var imageCapture: ImageCapture? = null + + private lateinit var cameraExecutor: ExecutorService override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { - val scanViewModel: ScanViewModel by viewModels() _binding = FragmentScanBinding.inflate(inflater, container, false) val root: View = binding.root - val textView: TextView = binding.textScan - scanViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + if (allPermissionsGranted()) { + startCamera() + } else { + requestPermissions() } + + binding.imageCaptureButton.setOnClickListener { takePhoto() } + + cameraExecutor = Executors.newSingleThreadExecutor() + +// val textView: TextView = binding.textScan +// viewModel.text.observe(viewLifecycleOwner) { +// textView.text = it +// } return root } + private fun takePhoto() { + // Get a stable reference of the modifiable image capture use case + val imageCapture = imageCapture ?: return + + // Create time stamped name and MediaStore entry. + val name = SimpleDateFormat(FILENAME_FORMAT, Locale.ENGLISH) + .format(System.currentTimeMillis()) + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, name) + put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") + put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") + } + + // Create output options object which contains file + metadata + val outputOptions = ImageCapture.OutputFileOptions + .Builder(requireContext().contentResolver, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + contentValues) + .build() + + // Set up image capture listener, which is triggered after photo has + // been taken + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Log.e(TAG, "Photo capture failed: ${exc.message}", exc) + } + + override fun + onImageSaved(output: ImageCapture.OutputFileResults){ + val msg = "Photo capture succeeded: ${output.savedUri}" + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + Log.d(TAG, msg) + } + } + ) + } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + + cameraProviderFuture.addListener({ + // Used to bind the lifecycle of cameras to the lifecycle owner + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + // Preview + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(binding.viewFinder.surfaceProvider) + } + + imageCapture = ImageCapture.Builder() + .build() + + val imageAnalyzer = ImageAnalysis.Builder() + .build() + .also { + it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma -> + Log.d(TAG, "Average luminosity: $luma") + }) + } + + // Select back camera as a default + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + // Unbind use cases before rebinding + cameraProvider.unbindAll() + + // Bind use cases to camera + cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture, imageAnalyzer) + + } catch(exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun requestPermissions() { + activityResultLauncher.launch(REQUIRED_PERMISSIONS) + } + + private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { + ContextCompat.checkSelfPermission( + requireContext(), it) == PackageManager.PERMISSION_GRANTED + } override fun onDestroyView() { super.onDestroyView() _binding = null + cameraExecutor.shutdown() + } + private val activityResultLauncher = + registerForActivityResult( + ActivityResultContracts.RequestMultiplePermissions()) + { permissions -> + // Handle Permission granted/rejected + var permissionGranted = true + permissions.entries.forEach { + if (it.key in REQUIRED_PERMISSIONS && !it.value) + permissionGranted = false + } + if (!permissionGranted) { + Toast.makeText(requireContext(), + "Permission request denied", + Toast.LENGTH_SHORT).show() + } else { + startCamera() + } + } + private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer { + + private fun ByteBuffer.toByteArray(): ByteArray { + rewind() // Rewind the buffer to zero + val data = ByteArray(remaining()) + get(data) // Copy the buffer into a byte array + return data // Return the byte array + } + + override fun analyze(image: ImageProxy) { + + val buffer = image.planes[0].buffer + val data = buffer.toByteArray() + val pixels = data.map { it.toInt() and 0xFF } + val luma = pixels.average() + + listener(luma) + + image.close() + } + } + companion object { + private const val TAG = "CameraXApp" + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" + private val REQUIRED_PERMISSIONS = + mutableListOf ( + "CAMERA" + ).apply { + add("WRITE_EXTERNAL_STORAGE") + }.toTypedArray() } } \ No newline at end of file diff --git a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt index 437135bf5c9e6a18cbc3ad3fb797d3f3c44125a5..7db1d29ea94c9aa15a8abc0bc4895565e0cc5bfd 100644 --- a/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt +++ b/app/src/main/java/pbd/tubes/exe_android/ui/scan/ScanViewModel.kt @@ -6,8 +6,8 @@ import androidx.lifecycle.ViewModel class ScanViewModel : ViewModel() { - private val _text = MutableLiveData<String>().apply { - value = "This is Scan Fragment" - } - val text: LiveData<String> = _text +// private val _text = MutableLiveData<String>().apply { +// value = "This is Scan Fragment" +// } +// val text: LiveData<String> = _text } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_scan.xml b/app/src/main/res/layout/fragment_scan.xml index b595956a6518f8278d54a822a2ffe6bd04a95da4..2b402d518ebf007879e01172bf3836b2899a255c 100644 --- a/app/src/main/res/layout/fragment_scan.xml +++ b/app/src/main/res/layout/fragment_scan.xml @@ -6,17 +6,21 @@ android:layout_height="match_parent" tools:context=".ui.scan.ScanFragment"> - <TextView - android:id="@+id/text_scan" + <androidx.camera.view.PreviewView + android:id="@+id/viewFinder" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" + android:layout_height="match_parent" /> + + <Button + android:id="@+id/image_capture_button" + android:layout_width="110dp" + android:layout_height="110dp" + android:layout_marginBottom="50dp" + android:elevation="2dp" + android:text="@string/take_photo" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fde62b26071bab584739dbfdde8a9ce7807ae3b2..2827f49dc0c69b9fb5141cb0ed3c1a82c83bdc7c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ <resources> - <string name="app_name">exe_android</string> + <string name="app_name">Bondoman</string> <string name="title_home">Home</string> <string name="title_dashboard">Dashboard</string> <string name="title_notifications">Notifications</string> @@ -15,4 +15,5 @@ <string name="transaction_nominal_text">Nominal</string> <string name="transaction_category">Kategori</string> <string name="transaction_location">Lokasi</string> + <string name="take_photo">ambil foto</string> </resources> \ No newline at end of file