diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9beff3ec9f2a87177ae5d8fe7e4221c43fd798af..0089e4fbb12a612f9ec58933fead21efc8ec8fdd 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -81,4 +81,12 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("androidx.camera:camera-camera2:1.3.2") + implementation("androidx.camera:camera-view:1.3.2") + implementation("androidx.camera:camera-lifecycle:1.3.2") + + implementation("androidx.activity:activity-ktx:1.8.2") + + } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da4b0268fb3bee01b0f1eed2e5458c2b9547e803..04b7e735f269a244b71dfb60ce0b6f0dec563645 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,9 +2,17 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> + <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.CAMERA" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <application diff --git a/app/src/main/java/com/example/bondoyap/service/LocationManager.kt b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..3196f9aa9dbadf66b1e00ea7082b75c1a8e45cbf --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/LocationManager.kt @@ -0,0 +1,61 @@ +package com.example.bondoyap.service + +import android.annotation.SuppressLint +import android.content.Context +import android.content.pm.PackageManager +import android.location.Location +import android.util.Log +import androidx.core.app.ActivityCompat +import androidx.fragment.app.FragmentActivity +import com.google.android.gms.location.LocationServices + +class LocationManager { + companion object { + fun haveLocationPermission(context: Context): Boolean { + return (ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + context, android.Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED) + } + + fun askLocationPermission(context: Context, activity: FragmentActivity) { + if (!haveLocationPermission(context)) { + ActivityCompat.requestPermissions( + activity, arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 + ) + } + } + + @SuppressLint("MissingPermission") + fun getLocation(context: Context): Location? { + var location: Location? = null + if (haveLocationPermission(context)) { + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(context) + Log.d("LocationManager", "Getting last location") + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + location = it + } + } + } + + if (location == null) { + Log.d("LocationManager", "Location is null") + } else { + Log.d( + "LocationManager", + "Get location at latitude: ${location?.latitude} and longitude: ${location?.longitude}" + ) + } + return location + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt index a7bf474abba94624133338ce218ad87295d7e584..803015586a080eda094a614661e240d63a0eebe1 100644 --- a/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt +++ b/app/src/main/java/com/example/bondoyap/service/api/ApiService.kt @@ -1,11 +1,15 @@ package com.example.bondoyap.service.api +import com.example.bondoyap.service.api.data.BillResponse import com.example.bondoyap.service.api.data.LoginRequest import com.example.bondoyap.service.api.data.LoginResponse import com.example.bondoyap.service.api.data.TokenResponse +import okhttp3.MultipartBody import retrofit2.Call import retrofit2.http.Body +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part interface ApiService { @POST("auth/login") @@ -19,6 +23,8 @@ interface ApiService { ): Call<TokenResponse> @POST("bill/upload") - fun uploadPicture( - ) + @Multipart + fun getBill( + @Part photoPart : MultipartBody.Part + ) : Call<BillResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..098c5e658bc2c42e7acb89deea7e3ff7514ea188 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/BillResponse.kt @@ -0,0 +1,5 @@ +package com.example.bondoyap.service.api.data + +data class BillResponse( + val items : Items +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt new file mode 100644 index 0000000000000000000000000000000000000000..cd01390409cf0b82f1638f536e0d814e988817be --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Item.kt @@ -0,0 +1,9 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Item( + val name : String, + @Json(name = "qty") + val quantity : Int, + val price : Double +) diff --git a/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt new file mode 100644 index 0000000000000000000000000000000000000000..6c779b17b1baebb2337f106ad94a51398ea47458 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/service/api/data/Items.kt @@ -0,0 +1,6 @@ +package com.example.bondoyap.service.api.data +import com.squareup.moshi.Json + +data class Items( + val items : List<Item> +) diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c7913390e762e7536716cb0e985dda5702e0357 --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScanResultFragment.kt @@ -0,0 +1,139 @@ +package com.example.bondoyap.ui.scanner + +import android.annotation.SuppressLint +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.example.bondoyap.databinding.FragmentScanResultBinding +import com.example.bondoyap.service.LocationManager +import com.example.bondoyap.service.api.data.Item +import com.example.bondoyap.ui.scanner.listAdapter.ScanResultListAdapter +import com.example.bondoyap.ui.transactions.TransactionsApplication +import com.example.bondoyap.ui.transactions.TransactionsViewModel +import com.example.bondoyap.ui.transactions.TransactionsViewModelFactory +import com.example.bondoyap.ui.transactions.data.Transactions +import com.google.android.gms.location.LocationServices +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + + +class ScanResultFragment : Fragment() { + // This property is only valid between onCreateView and + // onDestroyView. + private var _binding: FragmentScanResultBinding? = null + private val binding get() = _binding!! + private lateinit var navController: NavController + + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentScanResultBinding.inflate(inflater, container, false) + val root: View = binding.root + + navController = findNavController() + val scannerViewModel: ScannerViewModel by activityViewModels() + val items = scannerViewModel.items ?: throw Exception("Scan Result is null") + Log.d("ScanResult", "Result Received: $items") + + val recyclerView = binding.recyclerView + val adapter = ScanResultListAdapter() + recyclerView.adapter = adapter + recyclerView.layoutManager = LinearLayoutManager(requireContext()) + adapter.submitList(items) + + binding.cancelButton.setOnClickListener { + navController.popBackStack() + } + + binding.saveButton.setOnClickListener { + saveNota(items) + } + + return root + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun saveNota(items: List<Item>) { + try { + Log.d("ScanResult", "Saving note: $items") + saveNotaRepository(items) + Toast.makeText(requireContext(), "Nota berhasil disimpan", Toast.LENGTH_SHORT).show() + navController.popBackStack() + } catch (e: Exception) { + Toast.makeText(requireContext(), "Nota gagal disimpan", Toast.LENGTH_SHORT).show() + Log.e("ScanResult", "Saving note failed:", e) + } + } + + private fun saveNotaRepository(items: List<Item>) { + val dateDateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) + val titleDateFormat = SimpleDateFormat("dd_MM_yyyy_HH_mm_ss", Locale.getDefault()) + val currentTime = Date() + val currentDate = dateDateFormat.format(currentTime) + val title = titleDateFormat.format(currentTime) + + val value = items.sumOf { it.quantity * it.price } + + val transactionsViewModel: TransactionsViewModel by viewModels { + TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) + } + + LocationManager.askLocationPermission(requireContext(), requireActivity()) + Log.d("ScanResult", "Getting location") + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("ScanResult", "Saving note on database") + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } + } + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( + judul = "Scanner_${title}", + nominal = value, + isPemasukan = false, + tanggal = currentDate, + longitude = "", + latitude = "" + ) + transactionsViewModel.upsert(transaction) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt index 0996dc927745fd359a7ac320a90b845d8ffb9083..94353b0bae865f894553b87498625e90e009a073 100644 --- a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerFragment.kt @@ -1,37 +1,144 @@ package com.example.bondoyap.ui.scanner +import android.Manifest +import android.app.Activity +import android.content.ContentResolver +import android.content.Intent +import android.net.Uri 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 androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.camera.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +import androidx.camera.lifecycle.ProcessCameraProvider +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.ViewModelProvider +import androidx.fragment.app.activityViewModels +import androidx.navigation.NavController +import androidx.navigation.fragment.findNavController +import com.example.bondoyap.R import com.example.bondoyap.databinding.FragmentScannerBinding +import com.example.bondoyap.service.api.ApiClient +import com.example.bondoyap.service.api.data.BillResponse +import com.example.bondoyap.service.api.data.Items +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import retrofit2.Call +import retrofit2.Callback +import retrofit2.Response +import java.io.File +import java.io.FileOutputStream -class ScannerFragment : Fragment() { - - private var _binding: FragmentScannerBinding? = null +class ScannerFragment : Fragment() { // This property is only valid between onCreateView and // onDestroyView. + private var _binding: FragmentScannerBinding? = null private val binding get() = _binding!! + private lateinit var navController: NavController + + // Camera + private var isBackCamera = true + private var frozenPreview: Boolean = false + private var cameraImageFile: File? = null + private lateinit var imageCapture: ImageCapture + private lateinit var cameraLauncher: ActivityResultLauncher<String> + + // Gallery + private lateinit var changeImage: ActivityResultLauncher<Intent> + private lateinit var pickImageLauncher: ActivityResultLauncher<String> + + // Upload + private lateinit var cacheDir: File + private lateinit var contentResolver: ContentResolver + private lateinit var apiClient: ApiClient + override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { - val scannerViewModel = - ViewModelProvider(this).get(ScannerViewModel::class.java) - _binding = FragmentScannerBinding.inflate(inflater, container, false) val root: View = binding.root - val textView: TextView = binding.textScanner - scannerViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + navController = findNavController() + cacheDir = requireContext().cacheDir + contentResolver = requireContext().contentResolver + apiClient = ApiClient() + + cameraLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + startCamera() + } + } + + changeImage = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + try { + val data = it.data + val imgUri = data?.data ?: throw Exception("Image Uri is null") + Log.d("ImageInput", "Image Selected with URI: $imgUri") + + // Copying image from external directory to cache + val tempFile = File.createTempFile("Gallery_Image", ".jpg", cacheDir) + val inputStream = contentResolver.openInputStream(imgUri) + val outputStream = FileOutputStream(tempFile) + inputStream?.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + + uploadPhoto(tempFile) + } catch (e: Exception) { + Log.e("ImageInput", "Image Input Failed:", e) + } + } + } + + val pickImageIntent = + Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI) + pickImageLauncher = registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted -> + if (isGranted) { + changeImage.launch(pickImageIntent) + } + } + + imageCapture = ImageCapture.Builder().build() + cameraLauncher.launch(Manifest.permission.CAMERA) + + binding.switchCameraButton.setOnClickListener { + toggleCamera() + } + + binding.captureButton.setOnClickListener { + freezePreview() + } + + binding.galleryButton.setOnClickListener { + pickImageLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } + + binding.uploadButton.isClickable = false + binding.uploadButton.setOnClickListener { + cameraImageFile?.let { it1 -> uploadPhoto(it1) } } + return root } @@ -39,4 +146,108 @@ class ScannerFragment : Fragment() { super.onDestroyView() _binding = null } + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + cameraProviderFuture.addListener({ + val cameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder().build().also { mPreview -> + if (!frozenPreview) { + mPreview.setSurfaceProvider(binding.previewView.surfaceProvider) + } else { + mPreview.setSurfaceProvider(null) + } + } + + val imageCapture = ImageCapture.Builder() + .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) + .build() + + val cameraSelector = if (isBackCamera) { + CameraSelector.DEFAULT_BACK_CAMERA + } else { + CameraSelector.DEFAULT_FRONT_CAMERA + } + + try { + cameraProvider?.unbindAll() + cameraProvider?.bindToLifecycle(this, cameraSelector, preview, imageCapture) + + val tempFile = File.createTempFile("Camera_Image", ".jpg", cacheDir) + val outputOptions = ImageCapture.OutputFileOptions.Builder(tempFile).build() + imageCapture.takePicture(outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { + val uriImg = Uri.fromFile(tempFile) + cameraImageFile = tempFile + Log.d("CameraX", "Image captured: $uriImg") + } + + override fun onError(exception: ImageCaptureException) { + Log.e( + "CameraX", + "Error capturing image: ${exception.message}", + exception + ) + } + } + ) + } catch (e: Exception) { + Log.e("CameraX", "Starting Camera Failed:", e) + } + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun toggleCamera() { + isBackCamera = !isBackCamera + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun freezePreview() { + frozenPreview = !frozenPreview + if (!frozenPreview) { + binding.captureButton.text = "Capture" + binding.uploadButton.isClickable = false + } else { + binding.captureButton.text = "Retake" + binding.uploadButton.isClickable = true + } + cameraLauncher.launch(Manifest.permission.CAMERA) + } + + private fun uploadPhoto(photo: File) { + try { + val requestFile = photo.asRequestBody("image/*".toMediaTypeOrNull()) + val requestBody = MultipartBody.Part.createFormData("file", photo.name, requestFile) + val apiCall = apiClient.getApiService(requireContext()).getBill(requestBody) + apiCall.enqueue(object : Callback<BillResponse> { + override fun onResponse( + call: Call<BillResponse>, response: Response<BillResponse> + ) { + if (response.isSuccessful) { + val billResponse = + response.body() ?: throw Exception("Bill Response is Empty") + Log.d("BillUpload", "Server Response: $billResponse") + navigateScanResult(billResponse.items) + } else { + Log.e("BillUpload", "Error: ${response.code()}") + } + } + + override fun onFailure(call: Call<BillResponse>, t: Throwable) { + Log.e("BillUpload", "Failed to upload bill", t) + } + }) + } catch (e: Exception) { + Log.e("BillUpload", "Bill Upload Failed:", e) + } + } + + private fun navigateScanResult(items: Items) { + val scannerViewModel: ScannerViewModel by activityViewModels() + scannerViewModel.items = items.items + navController.navigate(R.id.action_to_scanResultFragment) + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt index ba02a0bc2a2ac16f37abfefc81ad9afdc7cd6a0f..3e659568fd7e9ca0d091f8b24e1c7703777b3dc6 100644 --- a/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/ScannerViewModel.kt @@ -3,11 +3,14 @@ package com.example.bondoyap.ui.scanner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import com.example.bondoyap.service.api.data.Item class ScannerViewModel : ViewModel() { + var items: List<Item>? = null private val _text = MutableLiveData<String>().apply { value = "This is Scanner Fragment" } val text: LiveData<String> = _text + } \ No newline at end of file diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt new file mode 100644 index 0000000000000000000000000000000000000000..15e214aa61d0894a9bc13b086ea6c9845571a7de --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemDiffCallback.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import androidx.recyclerview.widget.DiffUtil +import com.example.bondoyap.service.api.data.Item + +class ItemDiffCallback : DiffUtil.ItemCallback<Item>() { + override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean { + val isNameSame = oldItem.name == newItem.name + val isQuantitySame = oldItem.quantity == newItem.quantity + val isPriceSame = oldItem.price == newItem.price + return isNameSame && isQuantitySame && isPriceSame + } + + override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean { + return oldItem == newItem + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt new file mode 100644 index 0000000000000000000000000000000000000000..3c751de3299b6cb36e794fe7c31cf07999f5943c --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ItemViewHolder.kt @@ -0,0 +1,29 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.example.bondoyap.R +import com.example.bondoyap.service.api.data.Item + +class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(item: Item) { + val itemName: TextView = itemView.findViewById(R.id.item_name) + val itemQuantity: TextView = itemView.findViewById(R.id.item_quantity) + val itemPrice: TextView = itemView.findViewById(R.id.item_price) + + itemName.text = "Nama: ${item.name}" + itemQuantity.text = "Jumlah: ${item.quantity}" + itemPrice.text = "Harga: ${item.price}" + } + + companion object { + fun create(parent: ViewGroup): ItemViewHolder { + val view: View = LayoutInflater.from(parent.context) + .inflate(R.layout.recyclerview_scan_result, parent, false) + return ItemViewHolder(view) + } + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt new file mode 100644 index 0000000000000000000000000000000000000000..d60897087cbdcf1d42dc7263bc9495e249317a7e --- /dev/null +++ b/app/src/main/java/com/example/bondoyap/ui/scanner/listAdapter/ScanResultListAdapter.kt @@ -0,0 +1,17 @@ +package com.example.bondoyap.ui.scanner.listAdapter + +import android.view.ViewGroup +import androidx.recyclerview.widget.ListAdapter +import com.example.bondoyap.service.api.data.Item + +class ScanResultListAdapter : + ListAdapter<Item, ItemViewHolder>(ItemDiffCallback()) { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { + return ItemViewHolder.create(parent) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + val currentItem = getItem(position) + holder.bind(currentItem) + } +} diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt index 6463fa0928a6487d79dfda7d59da716d1f735aed..3179e81a058d04067bf17a8fb0807344e3d502bc 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/AddTransactionsFragment.kt @@ -1,25 +1,19 @@ package com.example.bondoyap.ui.transactions import android.R -import android.content.Context -import android.content.IntentFilter -import android.content.pm.PackageManager -import android.os.Build +import android.annotation.SuppressLint import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast -import androidx.annotation.RequiresApi -import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels -import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.example.bondoyap.databinding.FragmentAddTransactionsBinding -import com.example.bondoyap.service.api.Constants.ACTION_RANDOMIZE_TRANSACTIONS +import com.example.bondoyap.service.LocationManager import com.example.bondoyap.ui.transactions.data.Transactions -import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import java.text.SimpleDateFormat import java.util.Date @@ -37,14 +31,10 @@ class AddTransactionsFragment : Fragment() { TransactionsViewModelFactory((requireContext().applicationContext as TransactionsApplication).repository) } - private lateinit var fusedLocationProviderClient: FusedLocationProviderClient - private lateinit var latitude: String - private lateinit var longitude: String - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? ): View { _binding = FragmentAddTransactionsBinding.inflate(inflater, container, false) @@ -57,18 +47,7 @@ class AddTransactionsFragment : Fragment() { adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item) binding.spinnerKategori.adapter = adapter - fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) - - if ( - ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(requireActivity(), - arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 - ) - } + LocationManager.askLocationPermission(requireContext(), requireActivity()) binding.buttonSimpan.setOnClickListener { val isPemasukan: Boolean = when (binding.spinnerKategori.selectedItem.toString()) { @@ -91,31 +70,36 @@ class AddTransactionsFragment : Fragment() { 0.0 } - val location = fusedLocationProviderClient.lastLocation - location.addOnSuccessListener { - if(it != null) { - latitude = it.latitude.toString() - longitude = it.longitude.toString() - - val transaction: Transactions = Transactions( - judul = judul, - nominal = nominal, - isPemasukan = isPemasukan, - tanggal = currentDate, - longitude = longitude, - latitude = latitude - ) - transactionsViewModel.upsert(transaction) + LocationManager.askLocationPermission(requireContext(), requireActivity()) + + if (LocationManager.haveLocationPermission(requireContext())) { + Log.d("ScanResult", "Saving note on database") + Log.d("LocationManager", "Getting last location") + val fusedLocationProviderClient = + LocationServices.getFusedLocationProviderClient(requireContext()) + + @SuppressLint("MissingPermission") + val locationProvider = fusedLocationProviderClient.lastLocation + locationProvider.addOnSuccessListener { + if (it != null) { + Log.d( + "LocationManager", + "Updating location to latitude: ${it.latitude} and longitude: ${it.longitude}" + ) + val transaction = Transactions( + judul = judul, + nominal = nominal, + isPemasukan = isPemasukan, + tanggal = currentDate, + longitude = it.longitude.toString(), + latitude = it.latitude.toString() + ) + transactionsViewModel.upsert(transaction) + } } - } - - if( - ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED - ) { - val transaction: Transactions = Transactions( + } else { + Log.d("ScanResult", "Saving note on database") + val transaction = Transactions( judul = judul, nominal = nominal, isPemasukan = isPemasukan, @@ -126,7 +110,8 @@ class AddTransactionsFragment : Fragment() { transactionsViewModel.upsert(transaction) } - Toast.makeText(requireContext(), "Transaksi berhasil disimpan", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), "Transaksi berhasil disimpan", Toast.LENGTH_SHORT) + .show() binding.editTextJudul.text.clear() binding.editTextNominal.text.clear() diff --git a/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt index f72aaec034e554588d337ae3b8a2d80d30291e71..d0b9e61cd7c193ba3e5f41a0b3fb821c180a4094 100644 --- a/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt +++ b/app/src/main/java/com/example/bondoyap/ui/transactions/EditTransactionsFragment.kt @@ -1,8 +1,8 @@ package com.example.bondoyap.ui.transactions import android.R +import android.annotation.SuppressLint import android.app.AlertDialog -import android.content.pm.PackageManager import android.location.Address import android.location.Geocoder import android.os.Bundle @@ -14,11 +14,11 @@ import android.view.View import android.view.ViewGroup import android.widget.ArrayAdapter import android.widget.Toast -import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import com.example.bondoyap.databinding.FragmentEditTransactionsBinding +import com.example.bondoyap.service.LocationManager import com.example.bondoyap.ui.transactions.data.Transactions import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices @@ -48,7 +48,7 @@ class EditTransactionsFragment : Fragment() { ): View { _binding = FragmentEditTransactionsBinding.inflate(inflater, container, false) - var tanggal: String = "" + var tanggal = "" var latitude = "" var longitude = "" @@ -71,7 +71,8 @@ class EditTransactionsFragment : Fragment() { val originalTransaction: Transactions = transactionsViewModel.get(transactionId) binding.editTextJudul.text = SpannableStringBuilder(originalTransaction.judul) - binding.editTextNominal.text = SpannableStringBuilder(originalTransaction.nominal.toBigDecimal().toString()) + binding.editTextNominal.text = + SpannableStringBuilder(originalTransaction.nominal.toBigDecimal().toString()) if (originalTransaction.isPemasukan) { binding.spinnerKategori.setSelection(categories.indexOf(pemasukan)) @@ -101,31 +102,14 @@ class EditTransactionsFragment : Fragment() { } } + LocationManager.askLocationPermission(requireContext(), requireActivity()) fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(requireContext()) - if ( - ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(requireActivity(), - arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 - ) - } - binding.checkboxUpdateLokasi.setOnCheckedChangeListener { _, isChecked -> if (isChecked) { - if ( - ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_FINE_LOCATION) - != PackageManager.PERMISSION_GRANTED - && ActivityCompat.checkSelfPermission(requireContext(), android.Manifest.permission.ACCESS_COARSE_LOCATION) - != PackageManager.PERMISSION_GRANTED - ) { - ActivityCompat.requestPermissions(requireActivity(), - arrayOf(android.Manifest.permission.ACCESS_FINE_LOCATION), 100 - ) + if (!LocationManager.haveLocationPermission(requireContext())) { binding.checkboxUpdateLokasi.isChecked = false + LocationManager.askLocationPermission(requireContext(), requireActivity()) } } } @@ -148,7 +132,7 @@ class EditTransactionsFragment : Fragment() { 0.0 } - val transaction: Transactions = Transactions( + val transaction = Transactions( judul = judul, nominal = nominal, isPemasukan = isPemasukan, @@ -159,11 +143,12 @@ class EditTransactionsFragment : Fragment() { ) if (binding.checkboxUpdateLokasi.isChecked) { + @SuppressLint("MissingPermission") val location = fusedLocationProviderClient.lastLocation - location.addOnSuccessListener { - if (it != null) { - latitude = it.latitude.toString() - longitude = it.longitude.toString() + location.addOnSuccessListener { loc -> + if (loc != null) { + latitude = loc.latitude.toString() + longitude = loc.longitude.toString() transaction.latitude = latitude transaction.longitude = longitude @@ -200,14 +185,18 @@ class EditTransactionsFragment : Fragment() { } else { binding.spinnerKategori.setSelection(categories.indexOf(pengeluaran)) } - Toast.makeText(requireContext(), "Transaksi berhasil diperbarui", Toast.LENGTH_SHORT).show() + Toast.makeText( + requireContext(), + "Transaksi berhasil diperbarui", + Toast.LENGTH_SHORT + ).show() } } binding.buttonHapus.setOnClickListener { - val transaction: Transactions = Transactions( + val transaction = Transactions( judul = "", nominal = 0.0, isPemasukan = false, @@ -218,7 +207,8 @@ class EditTransactionsFragment : Fragment() { showConfirmationDialog("Hapus", "Apakah Anda yakin ingin menghapus transaksi ini?") { transactionsViewModel.delete(transaction) findNavController().navigate(com.example.bondoyap.R.id.navigation_transactions) - Toast.makeText(requireContext(), "Transaksi berhasil dihapus", Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), "Transaksi berhasil dihapus", Toast.LENGTH_SHORT) + .show() } } diff --git a/app/src/main/res/layout/fragment_scan_result.xml b/app/src/main/res/layout/fragment_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..972c342f9703685caafa1ddf6265ee9e7d1cc188 --- /dev/null +++ b/app/src/main/res/layout/fragment_scan_result.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".ui.scanner.ScanResultFragment"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recycler_view" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <!-- TODO: fix relative to bottom nav bar --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="72dp" + android:orientation="horizontal"> + + <Button + android:id="@+id/cancel_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/cancel_button" /> + + <Button + android:id="@+id/save_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/save_button" /> + + </LinearLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/fragment_scanner.xml b/app/src/main/res/layout/fragment_scanner.xml index 883e701ac332ec99a2870cc89a74d2e6a979e81f..4caebe4c9e62ac89b85ed9966c7389d519e607b6 100644 --- a/app/src/main/res/layout/fragment_scanner.xml +++ b/app/src/main/res/layout/fragment_scanner.xml @@ -6,17 +6,45 @@ android:layout_height="match_parent" tools:context=".ui.scanner.ScannerFragment"> - <TextView - android:id="@+id/text_scanner" + + <androidx.camera.view.PreviewView + android:id="@+id/preview_view" android:layout_width="match_parent" + android:layout_height="400dp" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/switch_camera_button" + android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:textAlignment="center" - android:textSize="20sp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + android:text="@string/switch_camera_button" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/capture_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/capture_button" + app:layout_constraintStart_toEndOf="@+id/switch_camera_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + <Button + android:id="@+id/gallery_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/gallery_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/capture_button" + app:layout_constraintTop_toBottomOf="@id/preview_view" /> + + + <Button + android:id="@+id/upload_button" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/upload_button" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/capture_button" /> </androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/recyclerview_scan_result.xml b/app/src/main/res/layout/recyclerview_scan_result.xml new file mode 100644 index 0000000000000000000000000000000000000000..c172e9daa24a17d34ac12b0e3de9263d366866c3 --- /dev/null +++ b/app/src/main/res/layout/recyclerview_scan_result.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/card_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="8dp" + app:cardBackgroundColor="#af5eff" + app:cardCornerRadius="8dp" + app:cardElevation="4dp"> + + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <TextView + android:id="@+id/item_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_quantity" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + + <TextView + android:id="@+id/item_price" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="16sp" /> + </LinearLayout> + +</androidx.cardview.widget.CardView> diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 27c15a681a39c24c4ffedbc1e5ff8e7864537988..983df0358bc32b2943927cbc28b36223c5fe0927 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -5,11 +5,11 @@ android:id="@+id/mobile_navigation" app:startDestination="@+id/navigation_transactions"> -<!-- <fragment--> -<!-- android:id="@+id/navigation_login"--> -<!-- android:name="com.example.bondoyap.ui.login.LoginActivity"--> -<!-- android:label="@string/title_login"--> -<!-- tools:layout="@layout/fragment_transactions" />--> + <!-- <fragment--> + <!-- android:id="@+id/navigation_login"--> + <!-- android:name="com.example.bondoyap.ui.login.LoginActivity"--> + <!-- android:label="@string/title_login"--> + <!-- tools:layout="@layout/fragment_transactions" />--> <fragment android:id="@+id/navigation_transactions" @@ -33,7 +33,19 @@ android:id="@+id/navigation_scanner" android:name="com.example.bondoyap.ui.scanner.ScannerFragment" android:label="@string/title_scanner" - tools:layout="@layout/fragment_scanner" /> + tools:layout="@layout/fragment_scanner"> + + <action + android:id="@+id/action_to_scanResultFragment" + app:destination="@id/navigation_scan_result" /> + + </fragment> + + <fragment + android:id="@+id/navigation_scan_result" + android:name="com.example.bondoyap.ui.scanner.ScanResultFragment" + android:label="@string/title_scan_result" + tools:layout="@layout/fragment_scan_result" /> <fragment android:id="@+id/navigation_graph" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d59eb366c2f88c41ccacee48df8917b49bf379b0..170df652f7855ce5cc6f9f8a650e558b427c700f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4,6 +4,7 @@ <string name="title_scanner">Scanner</string> <string name="title_graph">Graph</string> <string name="title_settings">Pengaturan</string> + <string name="title_scan_result">Scan Result</string> <string name="title_login">Login</string> <string name="prompt_email">Email</string> @@ -35,4 +36,12 @@ <string name="hint_lokasi">Enter Lokasi</string> <string name="pemasukan">Pemasukan</string> <string name="pengeluaran">Pengeluaran</string> + + <string name="capture_button">capture</string> + <string name="switch_camera_button">switch camera</string> + <string name="gallery_button">gallery</string> + <string name="upload_button">upload</string> + + <string name="cancel_button">cancel</string> + <string name="save_button">save</string> </resources> \ No newline at end of file