Skip to content
Snippets Groups Projects
Commit 77703eff authored by Rinaldy Adin's avatar Rinaldy Adin
Browse files

Revert "Merge branch 'feat/scanner' into 'dev'"

This reverts merge request !9
parent 7ee674a5
Branches
Tags
No related merge requests found
Showing
with 48 additions and 406 deletions
......@@ -54,11 +54,6 @@ dependencies {
implementation(libs.androidx.retrofit.gson)
implementation(libs.poi)
implementation(libs.poi.ooxml)
implementation(libs.camera.core)
implementation(libs.camera.lifecycle)
implementation(libs.camera.view)
implementation(libs.glide)
annotationProcessor(libs.glideCompiler)
implementation(libs.androidx.activity)
implementation(libs.play.services.location)
annotationProcessor(libs.androidx.room.compiler)
......
......@@ -3,8 +3,6 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
......
......@@ -88,9 +88,7 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD
appBarConfiguration = AppBarConfiguration(
setOf(
R.id.navigation_transactions,
R.id.navigation_settings,
R.id.navigation_scan
R.id.navigation_transactions
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
......
package com.example.abe.data.network
import android.content.Context
import okhttp3.MediaType
import okhttp3.MultipartBody
import okhttp3.RequestBody
import android.preference.PreferenceManager
import com.example.abe.R
import com.example.abe.services.AuthService
......@@ -12,17 +9,12 @@ import retrofit2.Callback
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
interface LoginResultCallback {
fun onSuccess(loginResponse: LoginResponse)
fun onFailure(errorMessage: String)
}
interface UploadResultCallback {
fun onSuccess(uploadResponse: ItemsRoot)
fun onFailure(errorMessage: String)
interface CheckAuthResultCallback {
fun onFailure()
}
......@@ -78,39 +70,6 @@ class Retrofit {
})
}
fun upload(context: Context, file: File, callback: UploadResultCallback) {
val scannerService = retrofit.create(ScannerService::class.java)
val sharedPreferences = context.getSharedPreferences("YourPreferenceName", Context.MODE_PRIVATE)
val authHeader = "Bearer " + sharedPreferences.getString("login_token", "")
// create RequestBody instance from file
val requestFile = RequestBody.create(
MediaType.parse("multipart/form-data"),
file
)
// MultipartBody.Part is used to send also the actual file name
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
val call: Call<ItemsRoot> = scannerService.uploadScan(authHeader, body)
call.enqueue(object: Callback<ItemsRoot> {
override fun onResponse(call: Call<ItemsRoot>, response: Response<ItemsRoot>) {
if (response.isSuccessful) {
response.body()?.let {
callback.onSuccess(it)
}
} else {
callback.onFailure("Scan failed")
}
}
override fun onFailure(call: Call<ItemsRoot>, t: Throwable) {
callback.onFailure("Failed to send request")
}
})
}
fun checkAuth(token: String, callback: CheckAuthResultCallback) {
val checkAuthService = retrofit.create(CheckAuthService::class.java)
val call: Call<CheckAuthResponse> = checkAuthService.checkAuth("Bearer $token")
......
package com.example.abe.data.network
import okhttp3.MultipartBody
import com.example.abe.data.network.LoginRequest
import com.example.abe.data.network.LoginResponse
import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Multipart
import retrofit2.http.POST
import retrofit2.http.Part
interface LoginService {
@POST("api/auth/login")
fun login(@Body user: LoginRequest) : Call<LoginResponse>
}
interface ScannerService {
@Multipart
@POST("api/bill/upload")
fun uploadScan(
@Header("Authorization") authHeader: String,
@Part file: MultipartBody.Part
): Call<ItemsRoot>
interface CheckAuthService {
@POST("api/auth/token")
fun checkAuth(@Header("Authorization") token: String) : Call<CheckAuthResponse>
......
......@@ -9,22 +9,8 @@ data class LoginResponse (
val token: String
)
data class TransactionItem(
val name: String,
val qty: Int,
val price: Double
)
data class ItemsContainer(
val items: List<TransactionItem>
)
data class ItemsRoot(
val items: ItemsContainer
)
data class CheckAuthResponse (
val nim: String,
val iat: String,
val exp: String
)
)
\ No newline at end of file
package com.example.abe.ui.settings
package com.example.abe.ui.notifications
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.example.abe.databinding.FragmentSettingsBinding
import com.example.abe.databinding.FragmentNotificationsBinding
class SettingsFragment : Fragment() {
class NotificationsFragment : Fragment() {
private var _binding: FragmentSettingsBinding? = null
private var _binding: FragmentNotificationsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
......@@ -21,12 +22,17 @@ class SettingsFragment : Fragment() {
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val settingsViewModel =
ViewModelProvider(this).get(SettingsViewModel::class.java)
val notificationsViewModel =
ViewModelProvider(this).get(NotificationsViewModel::class.java)
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
_binding = FragmentNotificationsBinding.inflate(inflater, container, false)
val root: View = binding.root
return binding.root
val textView: TextView = binding.textNotifications
notificationsViewModel.text.observe(viewLifecycleOwner) {
textView.text = it
}
return root
}
override fun onDestroyView() {
......
package com.example.abe.ui.settings
package com.example.abe.ui.notifications
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SettingsViewModel : ViewModel() {
class NotificationsViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is notifications Fragment"
......
package com.example.abe.ui.scanner
import android.Manifest
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.view.PreviewView
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.bumptech.glide.Glide
import com.example.abe.R
import androidx.camera.core.*
import androidx.fragment.app.viewModels
import com.example.abe.ABEApplication
import com.example.abe.data.network.Retrofit
import com.example.abe.data.network.ItemsRoot
import com.example.abe.data.network.TransactionItem
import com.example.abe.data.network.UploadResultCallback
import com.example.abe.databinding.FragmentScanBinding
import com.google.android.gms.location.FusedLocationProviderClient
import com.google.android.gms.location.LocationServices
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class ScannerFragment : Fragment(), UploadResultCallback {
private var _binding: FragmentScanBinding? = null
private val binding get() = _binding!!
private lateinit var cameraExecutor: ExecutorService
private lateinit var cameraView: PreviewView
private lateinit var imageCapture: ImageCapture
private lateinit var fusedLocationClient: FusedLocationProviderClient
private var latitude = 0.0
private var longitude = 0.0
private lateinit var user: String
private val viewModel: ScannerViewModel by viewModels {
ScannerViewModelFactory((activity?.application as ABEApplication).repository)
}
private val requestPermissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
if (permissions.all { it.value }) {
startCamera()
}
}
private val openGalleryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val imageUri = result.data?.data
val imageFile = File(imageUri?.path.toString())
attemptUpload(imageFile)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentScanBinding.inflate(inflater, container, false)
cameraView = binding.camera
cameraExecutor = Executors.newSingleThreadExecutor()
requestPermissions()
val sharedPref = activity?.getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE)
user = sharedPref?.getString("user", "").toString()
binding.captureButton.setOnClickListener {
takePicture()
}
binding.galleryPreviewButton.setOnClickListener {
openGallery()
}
return binding.root
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(cameraView.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
viewLifecycleOwner, cameraSelector, preview, imageCapture
)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(requireContext()))
}
private fun attemptUpload(imageFile: File) {
val retrofit = Retrofit()
val context = requireContext()
retrofit.upload(context, imageFile, this)
}
private fun showPreviewDialog(imageUri: Uri) {
val dialog = Dialog(requireContext()).apply {
setContentView(R.layout.dialog_image_preview)
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
}
val imageView = dialog.findViewById<ImageView>(R.id.dialog_image_view)
Glide.with(requireContext())
.load(imageUri)
.into(imageView)
val confirmButton = dialog.findViewById<Button>(R.id.confirm_button)
val cancelButton = dialog.findViewById<Button>(R.id.cancel_button)
confirmButton.setOnClickListener {
val filePath = imageUri.path
if (filePath != null) {
val imageFile = File(filePath)
attemptUpload(imageFile)
val msg = "Photo sent!"
Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
dialog.dismiss()
} else {
Log.e(TAG, "File path is null for imageUri: $imageUri")
dialog.dismiss()
}
}
cancelButton.setOnClickListener {
dialog.dismiss()
}
dialog.show()
}
private fun takePicture() {
val imageCapture = imageCapture
val photoFile = File(
requireContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES),
SimpleDateFormat(FILENAME_FORMAT, Locale.US).format(System.currentTimeMillis()) + ".jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
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) {
// Proceed with further operations
val savedUri = Uri.fromFile(photoFile)
showPreviewDialog(savedUri)
}
}
)
}
private fun getLastLocation() {
fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity())
if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
fusedLocationClient.lastLocation
.addOnSuccessListener { location: android.location.Location? ->
location?.let {
latitude = location.latitude
longitude = location.longitude
Log.d(TAG, "Latitude: $latitude, Longitude: $longitude")
// Now you have latitude and longitude, pass them to insertTransaction
}
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to get location: ${e.message}", e)
}
} else {
// Handle the case where location permission is not granted
Log.e(TAG, "Location permission not granted")
// You may want to request the permission again or handle it in some other way
}
}
override fun onSuccess(uploadResponse: ItemsRoot) {
uploadResponse.items.items.forEach {item ->
viewModel.insertTransaction(user, item, latitude, longitude)
}
}
override fun onFailure(errorMessage: String) {
println("Scan Error! $errorMessage")
}
private fun openGallery() {
val intent = Intent(Intent.ACTION_PICK)
intent.type = "image/*"
openGalleryLauncher.launch(intent)
}
private fun allPermissionsGranted() = ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED
private fun requestPermissions() {
requestPermissionLauncher.launch(arrayOf(Manifest.permission.CAMERA))
}
override fun onDestroyView() {
super.onDestroyView()
cameraExecutor.shutdown()
_binding = null
}
companion object {
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
private const val TAG = "ScannerFragment"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (allPermissionsGranted()) {
startCamera()
getLastLocation()
} else {
ActivityCompat.requestPermissions(
requireActivity(), REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
}
}
package com.example.abe.ui.scanner
import androidx.lifecycle.ViewModel
import com.example.abe.data.TransactionRepository
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.Dispatchers
import com.example.abe.data.Transaction
import com.example.abe.data.network.TransactionItem
import java.util.Date
class ScannerViewModel(private val transactionRepository: TransactionRepository):
ViewModel() {
fun insertTransaction(user: String, item: TransactionItem, lat: Double, long: Double) = viewModelScope.launch(Dispatchers.IO) {
val transaction = Transaction(
id = 0,
email = user,
title = item.name,
amount = (item.qty * item.price).toInt(),
isExpense = (item.qty * item.price).toInt() < 0,
timestamp = Date(),
latitude = lat,
longitude = long,
location = "Unknown",
)
transactionRepository.insert(transaction)
}
}
class ScannerViewModelFactory(private val repository: TransactionRepository) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(ScannerViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return ScannerViewModel(repository) as T
}
throw IllegalArgumentException("Unknown viewmodel class")
}
}
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<group android:scaleX="1.0434783"
android:scaleY="1.0434783">
<path
android:fillColor="@android:color/white"
android:pathData="M3,4V1h2v3h3v2H5v3H3V6H0V4H3zM6,10V7h3V4h7l1.83,2H21c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2H5c-1.1,0 -2,-0.9 -2,-2V10H6zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5s-5,2.24 -5,5S10.24,19 13,19zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2s-1.43,-3.2 -3.2,-3.2S9.8,12.23 9.8,14z"/>
</group>
</vector>
app/src/main/res/drawable-hdpi/ic_gallery.png

606 B

app/src/main/res/drawable-hdpi/ic_logout.png

703 B

app/src/main/res/drawable-hdpi/ic_randomize.png

1.08 KiB

app/src/main/res/drawable-mdpi/ic_gallery.png

393 B

app/src/main/res/drawable-mdpi/ic_logout.png

414 B

app/src/main/res/drawable-mdpi/ic_randomize.png

633 B

app/src/main/res/drawable-xhdpi/ic_gallery.png

787 B

app/src/main/res/drawable-xhdpi/ic_logout.png

801 B

app/src/main/res/drawable-xhdpi/ic_randomize.png

1.25 KiB

0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment