diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2ee2962cb09d26470512fe163883f2bd43f8a553..cbf15dec27d23379f24fbf910881a32c5b8bc9c9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,11 +54,15 @@ dependencies { implementation(libs.androidx.retrofit.gson) implementation(libs.poi) implementation(libs.poi.ooxml) - implementation(libs.androidx.activity) - implementation(libs.play.services.location) 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) + implementation(libs.okhttp) + implementation(libs.logging.interceptor) annotationProcessor(libs.androidx.room.compiler) kapt(libs.androidx.room.compiler) implementation(libs.androidx.legacy.support.v4) diff --git a/app/src/main/java/com/example/abe/MainActivity.kt b/app/src/main/java/com/example/abe/MainActivity.kt index e0b9c7d5de845c43a164e52fde42fb089d274afb..9a09b4bb1ea3d304de725d64f039420b8466369a 100644 --- a/app/src/main/java/com/example/abe/MainActivity.kt +++ b/app/src/main/java/com/example/abe/MainActivity.kt @@ -94,8 +94,9 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD appBarConfiguration = AppBarConfiguration( setOf( R.id.navigation_transactions, + R.id.navigation_graph, R.id.navigation_settings, - R.id.navigation_graph + R.id.navigation_scan ) ) setupActionBarWithNavController(navController, appBarConfiguration) diff --git a/app/src/main/java/com/example/abe/data/network/Retrofit.kt b/app/src/main/java/com/example/abe/data/network/Retrofit.kt index 992568b5827e4826620040170b84655550e69947..fe589397ae144b2cd8e8805ab8053fc31301d6aa 100644 --- a/app/src/main/java/com/example/abe/data/network/Retrofit.kt +++ b/app/src/main/java/com/example/abe/data/network/Retrofit.kt @@ -1,20 +1,33 @@ package com.example.abe.data.network import android.content.Context -import android.preference.PreferenceManager +import android.util.Log import com.example.abe.R -import com.example.abe.services.AuthService +import com.google.gson.Gson +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import okhttp3.RequestBody.Companion.asRequestBody import retrofit2.Call import retrofit2.Callback import retrofit2.Response import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory +import java.io.File +import java.net.URLConnection + interface LoginResultCallback { fun onSuccess(loginResponse: LoginResponse) fun onFailure(errorMessage: String) } +interface UploadResultCallback { + fun onSuccess(uploadResponse: ItemsRoot) + fun onFailure(errorMessage: String) +} + interface CheckAuthResultCallback { fun onFailure() } @@ -44,8 +57,15 @@ class CallBack<T> : Callback<T> { class Retrofit { + private val logger = HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BODY } + + private val client = OkHttpClient.Builder() + .addInterceptor(logger) + .build() + private val retrofit = Retrofit.Builder() .baseUrl("https://pbd-backend-2024.vercel.app/") + .client(client) .addConverterFactory(GsonConverterFactory.create()) .build() @@ -70,6 +90,53 @@ class Retrofit { }) } + fun upload(context: Context, file: File, callback: UploadResultCallback) { + val scannerService = retrofit.create(ScannerService::class.java) + val sharedPreferences = context.getSharedPreferences(context.getString(R.string.preference_file_key), Context.MODE_PRIVATE) + val authHeader = "Bearer " + sharedPreferences.getString("login_token", "") + + // Determine the MIME type of the file + val mimeType = URLConnection.guessContentTypeFromName(file.name) + if (mimeType == null) { + Log.e("ABE-PHO", "Could not determine MIME type of file") + return + } + + // create RequestBody instance from file + val requestFile = file + .asRequestBody(mimeType.toMediaTypeOrNull()) + + + // 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>) { +// Log.d("ABE-PHO", "response: " + Gson().toJson(response.body())) +// Log.d("ABE-PHO", "error: " + Gson().toJson(response.errorBody())) +// Log.d("ABE-PHO", "code: " + response.code()) +// Log.d("ABE-PHO", "headers: " + response.headers()) +// Log.d("ABE-PHO", "message: " + response.message()) + + if (response.isSuccessful) { + response.body()?.let { + callback.onSuccess(it) + } + } else { + callback.onFailure("Upload failed") + } + } + + override fun onFailure(call: Call<ItemsRoot>, t: Throwable) { + Log.d("ABE-PHO", "Failed to send request: " + t.message) + callback.onFailure("Failed to send photo") + } + }) + } + + fun checkAuth(token: String, callback: CheckAuthResultCallback) { val checkAuthService = retrofit.create(CheckAuthService::class.java) val call: Call<CheckAuthResponse> = checkAuthService.checkAuth("Bearer $token") diff --git a/app/src/main/java/com/example/abe/data/network/Services.kt b/app/src/main/java/com/example/abe/data/network/Services.kt index 0b8ac796833aa606c6fa271c56a06e51524dce81..42eff784a360a550e29b48501ff71afdd8d988ba 100644 --- a/app/src/main/java/com/example/abe/data/network/Services.kt +++ b/app/src/main/java/com/example/abe/data/network/Services.kt @@ -1,17 +1,27 @@ package com.example.abe.data.network -import com.example.abe.data.network.LoginRequest -import com.example.abe.data.network.LoginResponse +import okhttp3.MultipartBody 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> diff --git a/app/src/main/java/com/example/abe/data/network/Types.kt b/app/src/main/java/com/example/abe/data/network/Types.kt index 2e73e38be5dbde69407dd20a52082a14ade0fedd..e423b33a53a42e4b94da8eb9e738d55f86da2f47 100644 --- a/app/src/main/java/com/example/abe/data/network/Types.kt +++ b/app/src/main/java/com/example/abe/data/network/Types.kt @@ -9,8 +9,22 @@ 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 +) diff --git a/app/src/main/java/com/example/abe/ui/notifications/NotificationsFragment.kt b/app/src/main/java/com/example/abe/ui/notifications/NotificationsFragment.kt deleted file mode 100644 index d41dfc9b49a995133671578d2d2d103fdc22b18d..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/abe/ui/notifications/NotificationsFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -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.FragmentNotificationsBinding - -class NotificationsFragment : Fragment() { - - private var _binding: FragmentNotificationsBinding? = null - - // This property is only valid between onCreateView and - // onDestroyView. - private val binding get() = _binding!! - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val notificationsViewModel = - ViewModelProvider(this).get(NotificationsViewModel::class.java) - - _binding = FragmentNotificationsBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textNotifications - notificationsViewModel.text.observe(viewLifecycleOwner) { - textView.text = it - } - return root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/ui/scanner/ScannerFragment.kt b/app/src/main/java/com/example/abe/ui/scanner/ScannerFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..164acc5667023182b516ed7a3bdbd417670e7a06 --- /dev/null +++ b/app/src/main/java/com/example/abe/ui/scanner/ScannerFragment.kt @@ -0,0 +1,373 @@ +package com.example.abe.ui.scanner + +import android.Manifest +import android.annotation.SuppressLint +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.location.Address +import android.location.Geocoder +import android.location.LocationManager +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.core.CameraSelector +import androidx.camera.core.ImageCapture +import androidx.camera.core.ImageCaptureException +import androidx.camera.core.Preview +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 androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController +import com.bumptech.glide.Glide +import com.example.abe.ABEApplication +import com.example.abe.R +import com.example.abe.data.network.ItemsRoot +import com.example.abe.data.network.Retrofit +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 var useDefaultLocation = false + + private val DEFAULT_LATITUDE = -6.892382 + private val DEFAULT_LONGINTUDE = 107.608352 + + private lateinit var user: String + + private val viewModel: ScannerViewModel by viewModels { + ScannerViewModelFactory((activity?.application as ABEApplication).repository) + } + + private var isRequestingPermission = false + + private var uploadResponse: ItemsRoot? = null + + private val requestCameraPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + isRequestingPermission = false + if (isGranted) { + startCamera() + } else { + Toast.makeText( + requireContext(), + "Please allow access to camera to use scanner", + Toast.LENGTH_LONG + ).show() + } + } + + private fun uriToFile(imageUri: Uri): File { + val context = requireContext() + val inputStream = context.contentResolver.openInputStream(imageUri) + val tempFile = File.createTempFile("upload", ".jpg", context.cacheDir).apply { + outputStream().use { fileOut -> + inputStream?.copyTo(fileOut) + } + } + inputStream?.close() + return tempFile + } + + private val openGalleryLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val imageUri = result.data?.data + if (imageUri != null) { + val imageFile = uriToFile(imageUri) + attemptUpload(imageFile) + + val msg = "Uploading photo, please wait" + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + } else { + val msg = "Failed to fetch image from gallery" + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentScanBinding.inflate(inflater, container, false) + cameraView = binding.camera + cameraExecutor = Executors.newSingleThreadExecutor() + + if (!cameraPermissionGranted()) { + requestCameraPermission() + } else { + startCamera() + } + + 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 + } + + companion object { + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" + } + + 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) { + exc.printStackTrace() + } + + }, 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 = "Uploading photo, please wait" + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + + dialog.dismiss() + } else { + 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) { + Toast.makeText(requireContext(), "Photo capture failed", Toast.LENGTH_SHORT) + .show() + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + val savedUri = Uri.fromFile(photoFile) + showPreviewDialog(savedUri) + } + } + ) + } + + private fun setLocationAsDefault() { + latitude = DEFAULT_LATITUDE + longitude = DEFAULT_LONGINTUDE + useDefaultLocation = true + } + + @SuppressLint("MissingPermission") + private fun getCurrentLocationAndInsertTrx() { + fusedLocationClient = LocationServices.getFusedLocationProviderClient(requireActivity()) + + if (checkLocationPermissions() && checkIfLocationEnabled()) { + fusedLocationClient.lastLocation + .addOnCompleteListener(requireActivity()) { task -> + val location = task.result + if (location != null) { + latitude = location.latitude + longitude = location.longitude + } else { + setLocationAsDefault() + } + insertItems() + } + } else { + setLocationAsDefault() + } + } + + private fun checkLocationPermissions(): Boolean { + return ActivityCompat.checkSelfPermission( + requireActivity(), Manifest.permission.ACCESS_COARSE_LOCATION + ) == PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission( + requireActivity(), Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + } + + private fun checkIfLocationEnabled(): Boolean { + val locationManager: LocationManager = + requireActivity().getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || locationManager.isProviderEnabled( + LocationManager.NETWORK_PROVIDER + ) + } + + private fun askForLocationPermissions() { + requestLocationLauncher.launch( + arrayOf( + Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION + ) + ) + } + + private val requestLocationLauncher = + registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> + var granted = false + permissions.entries.forEach { + if (it.value) granted = true + } + if (granted) { + getCurrentLocationAndInsertTrx() + } else { + setLocationAsDefault() + insertItems() + } + } + + override fun onSuccess(uploadResponse: ItemsRoot) { + this.uploadResponse = uploadResponse + + if (checkLocationPermissions()) { + getCurrentLocationAndInsertTrx() + } else { + askForLocationPermissions() + } + } + + fun insertItems() { + val geocoder = Geocoder(requireContext(), Locale.getDefault()) + val locationList: MutableList<Address> = + geocoder.getFromLocation(latitude, longitude, 1) ?: mutableListOf<Address>() + val location = + if (!useDefaultLocation && locationList.size > 0) (locationList[0].getAddressLine(0)) else "Unknown location" + + uploadResponse?.items?.items?.forEach { item -> + viewModel.insertTransaction(user, item, latitude, longitude, location) + } + val msg = "New transactions added!" + Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + + findNavController().navigate(R.id.action_navigation_scanner_to_navigation_transactions) + uploadResponse = null + } + + override fun onFailure(errorMessage: String) { + Toast.makeText(requireContext(), "Upload failed", Toast.LENGTH_SHORT).show() + Log.e("ABE-PHO", errorMessage) + } + + private fun openGallery() { + val intent = Intent(Intent.ACTION_PICK) + intent.type = "image/*" + openGalleryLauncher.launch(intent) + } + + private fun cameraPermissionGranted() = ContextCompat.checkSelfPermission( + requireContext(), Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + private fun requestCameraPermission() { + isRequestingPermission = true + requestCameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + + override fun onDestroyView() { + super.onDestroyView() + cameraExecutor.shutdown() + _binding = null + } +} diff --git a/app/src/main/java/com/example/abe/ui/scanner/ScannerViewModel.kt b/app/src/main/java/com/example/abe/ui/scanner/ScannerViewModel.kt new file mode 100644 index 0000000000000000000000000000000000000000..5cfd8170db1d62a93d9160da905d379aa34d244a --- /dev/null +++ b/app/src/main/java/com/example/abe/ui/scanner/ScannerViewModel.kt @@ -0,0 +1,41 @@ +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, location: String) = 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 = location, + ) + 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") + } +} diff --git a/app/src/main/java/com/example/abe/ui/notifications/NotificationsViewModel.kt b/app/src/main/java/com/example/abe/ui/settings/SettingsViewModel.kt similarity index 75% rename from app/src/main/java/com/example/abe/ui/notifications/NotificationsViewModel.kt rename to app/src/main/java/com/example/abe/ui/settings/SettingsViewModel.kt index 56b1bbe6ff5e1f05774b1c318c49465fe6ae5574..e0de351d8ea4503d22739cc25d035ff851b6f7a1 100644 --- a/app/src/main/java/com/example/abe/ui/notifications/NotificationsViewModel.kt +++ b/app/src/main/java/com/example/abe/ui/settings/SettingsViewModel.kt @@ -1,10 +1,10 @@ -package com.example.abe.ui.notifications +package com.example.abe.ui.settings import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -class NotificationsViewModel : ViewModel() { +class SettingsViewModel : ViewModel() { private val _text = MutableLiveData<String>().apply { value = "This is notifications Fragment" diff --git a/app/src/main/java/com/example/abe/ui/transactions/TransactionsAdapter.kt b/app/src/main/java/com/example/abe/ui/transactions/TransactionsAdapter.kt index 96eeae3a9bc2f6ab3a8979ed7afed44e43c50f06..da0124b12db5ec09ad008dd4b908c56a8136bd0a 100644 --- a/app/src/main/java/com/example/abe/ui/transactions/TransactionsAdapter.kt +++ b/app/src/main/java/com/example/abe/ui/transactions/TransactionsAdapter.kt @@ -60,6 +60,10 @@ class TransactionsAdapter(private val itemClickListener: TransactionFragment.Ite flImageContainer.setBackgroundColor(context.getColor(R.color.secondary)) ivTrxIcon.setImageResource(R.drawable.ic_circle_arrow_down) tvAmount.setTextColor(context.getColor(R.color.success)) + } else { + flImageContainer.setBackgroundColor(context.getColor(R.color.primary)) + ivTrxIcon.setImageResource(R.drawable.ic_circle_arrow_up) + tvAmount.setTextColor(context.getColor(R.color.destructive)) } tvTrxTitle.text = trx.title diff --git a/app/src/main/res/drawable-anydpi/ic_gallery.xml b/app/src/main/res/drawable-anydpi/ic_gallery.xml new file mode 100644 index 0000000000000000000000000000000000000000..054d3d09d1ff1a78a3c50c1e68db22d6d89d99b2 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_gallery.xml @@ -0,0 +1,14 @@ +<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> diff --git a/app/src/main/res/drawable-hdpi/ic_gallery.png b/app/src/main/res/drawable-hdpi/ic_gallery.png new file mode 100644 index 0000000000000000000000000000000000000000..199c0ce52e5a1598f24082e371615275ba2d8d02 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_gallery.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_logout.png b/app/src/main/res/drawable-hdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..9331e62770bdc4c74fbcedcd7ac57a67c502fcfb Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_randomize.png b/app/src/main/res/drawable-hdpi/ic_randomize.png new file mode 100644 index 0000000000000000000000000000000000000000..667f0c88fcdf83348117bd3a479618e7f5533689 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_randomize.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_gallery.png b/app/src/main/res/drawable-mdpi/ic_gallery.png new file mode 100644 index 0000000000000000000000000000000000000000..ba50d7049809d681e0774ccf7e948e73939023f3 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_gallery.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_logout.png b/app/src/main/res/drawable-mdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..d55f5e0a6c9ede1869e96fc291687bb47f3dd642 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_randomize.png b/app/src/main/res/drawable-mdpi/ic_randomize.png new file mode 100644 index 0000000000000000000000000000000000000000..28fe11ceadb9ba92f3f46a527c9036f251a7f9cf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_randomize.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_gallery.png b/app/src/main/res/drawable-xhdpi/ic_gallery.png new file mode 100644 index 0000000000000000000000000000000000000000..b150affe38e4704e55c6fb478f4721a0ff085eb3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_gallery.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_logout.png b/app/src/main/res/drawable-xhdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..0a31e5c76a1adb317743c9cfc29a2efb8d8eaa81 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_randomize.png b/app/src/main/res/drawable-xhdpi/ic_randomize.png new file mode 100644 index 0000000000000000000000000000000000000000..69556c93f3150ba6ecd99046b83d74ae3e625400 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_randomize.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_gallery.png b/app/src/main/res/drawable-xxhdpi/ic_gallery.png new file mode 100644 index 0000000000000000000000000000000000000000..3ee421e0604f237e0db30b9f934b7920a91c1d5d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_gallery.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_logout.png b/app/src/main/res/drawable-xxhdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..55ffdf3d403032f16eec496e96c7eeec41668f6a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_randomize.png b/app/src/main/res/drawable-xxhdpi/ic_randomize.png new file mode 100644 index 0000000000000000000000000000000000000000..a02988c35002ee927718e550687c6ef2665f665f Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_randomize.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_logout.png b/app/src/main/res/drawable-xxxhdpi/ic_logout.png new file mode 100644 index 0000000000000000000000000000000000000000..52878a94028e2718d194435ed96be10c7ffb023f Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_logout.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_randomize.png b/app/src/main/res/drawable-xxxhdpi/ic_randomize.png new file mode 100644 index 0000000000000000000000000000000000000000..c901103051b2216baa691432683db052d66bd96d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_randomize.png differ diff --git a/app/src/main/res/drawable/bottom_bar_scan.xml b/app/src/main/res/drawable/bottom_bar_scan.xml new file mode 100644 index 0000000000000000000000000000000000000000..00381290052c3ecaea4e18b83de11c40b8e0c44b --- /dev/null +++ b/app/src/main/res/drawable/bottom_bar_scan.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="#8346AA"/> + <corners android:topLeftRadius="16dp" android:topRightRadius="16dp"/> +</shape> diff --git a/app/src/main/res/drawable/scan_button.xml b/app/src/main/res/drawable/scan_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..a5fb15718943d7a69357567e61ee2001f7e5929e --- /dev/null +++ b/app/src/main/res/drawable/scan_button.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:top="2dp" android:bottom="2dp" android:left="2dp" android:right="2dp"> + <shape android:shape="oval"> + <solid android:color="@color/white"/> + <size android:width="68dp" android:height="68dp"/> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/layout/dialog_image_preview.xml b/app/src/main/res/layout/dialog_image_preview.xml new file mode 100644 index 0000000000000000000000000000000000000000..d20c44d11ff527c054858c99aad61fc1407708bd --- /dev/null +++ b/app/src/main/res/layout/dialog_image_preview.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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"> + + + <ImageView + android:id="@+id/dialog_image_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" + tools:srcCompat="@tools:sample/avatars" /> + + <Button + android:id="@+id/confirm_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="48dp" + android:text="Confirm" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/cancel_button" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintStart_toStartOf="parent" /> + + <Button + android:id="@+id/cancel_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Cancel" + app:layout_constraintBottom_toBottomOf="@+id/confirm_button" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/confirm_button" + app:layout_constraintTop_toTopOf="@+id/confirm_button" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_notifications.xml b/app/src/main/res/layout/fragment_notifications.xml deleted file mode 100644 index d41793572bb3b8347ec4bced74b7bd4a43bed5d4..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_notifications.xml +++ /dev/null @@ -1,22 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<androidx.constraintlayout.widget.ConstraintLayout 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" - tools:context=".ui.notifications.NotificationsFragment"> - - <TextView - android:id="@+id/text_notifications" - 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" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> -</androidx.constraintlayout.widget.ConstraintLayout> \ 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 new file mode 100644 index 0000000000000000000000000000000000000000..f3b6fe9600caee95ac502101998fdb111b87a32d --- /dev/null +++ b/app/src/main/res/layout/fragment_scan.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/scan_screen" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.camera.view.PreviewView + android:id="@+id/camera" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintBottom_toTopOf="@+id/bottom_bar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.5" + app:layout_constraintVertical_chainStyle="packed"> + + </androidx.camera.view.PreviewView> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/bottom_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/bottom_bar_scan" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <Button + android:id="@+id/captureButton" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_marginTop="16dp" + android:layout_marginBottom="16dp" + android:background="@drawable/scan_button" + android:backgroundTint="#FFFFFF" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <ImageButton + android:id="@+id/galleryPreviewButton" + android:layout_width="52dp" + android:layout_height="52dp" + android:adjustViewBounds="true" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="Get Photo from Gallery" + android:padding="10dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_gallery" + app:layout_constraintBottom_toBottomOf="@+id/captureButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/captureButton" + app:layout_constraintTop_toTopOf="@+id/captureButton" /> + + + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/menu/bottom_nav_menu.xml b/app/src/main/res/menu/bottom_nav_menu.xml index cd5cb7f7c224557b131277efbce9121f5c666bb1..58927cd8ee50852868f44146fbb513194849ba1b 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -6,10 +6,10 @@ android:icon="@drawable/ic_transactions" android:title="Transaction" /> -<!-- <item--> -<!-- android:id="@+id/navigation_scan"--> -<!-- android:icon="@drawable/ic_scan"--> -<!-- android:title="Scan" />--> + <item + android:id="@+id/navigation_scan" + android:icon="@drawable/ic_scan" + android:title="Scan" /> <item android:id="@+id/navigation_graph" diff --git a/app/src/main/res/mipmap-hdpi/ic_add_image.png b/app/src/main/res/mipmap-hdpi/ic_add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..e19abb5f6e9e58db13a2be2911c1af6f1c9900da Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_add_image.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_add_image.png b/app/src/main/res/mipmap-mdpi/ic_add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..697de1a29c461476970a0f0fc8816e87b93300fa Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_add_image.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_add_image.png b/app/src/main/res/mipmap-xhdpi/ic_add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..a848fd35eb0e6da98b11936e14b642320e93a1b7 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_add_image.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_add_image.png b/app/src/main/res/mipmap-xxhdpi/ic_add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..964d86c27a91b5c1944bb7be62c4348092b4ba75 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_add_image.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_add_image.png b/app/src/main/res/mipmap-xxxhdpi/ic_add_image.png new file mode 100644 index 0000000000000000000000000000000000000000..cbcc9c81cb77e8fb8a77de5a71b8ea0512b1064f Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_add_image.png differ diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 6b817e8f061acda53c67e1547f0709d95b6e4fce..db6d987e9ce1229fcf6758fbb7477e47c86d59bd 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -26,11 +26,19 @@ tools:layout="@layout/fragment_form_transaction" /> -<!-- <fragment--> -<!-- android:id="@+id/navigation_scan"--> -<!-- android:name="com.example.abe.ui.scanner.ScannerFragment"--> -<!-- android:label="Scan"--> -<!-- tools:layout="@layout/fragment_scan" />--> + <fragment + android:id="@+id/navigation_scan" + android:name="com.example.abe.ui.scanner.ScannerFragment" + android:label="Scan" + tools:layout="@layout/fragment_scan"> + + <action + android:id="@+id/action_navigation_scanner_to_navigation_transactions" + app:destination="@+id/navigation_transactions" + app:popUpTo="@id/navigation_transactions" + app:popUpToInclusive="true" + /> + </fragment> <fragment android:id="@+id/navigation_graph" @@ -39,10 +47,6 @@ tools:layout="@layout/fragment_graph" /> -<!-- <fragment--> -<!-- android:id="@+id/navigation_graph"--> -<!-- android:label="Graph" />--> - <fragment android:id="@+id/navigation_settings" android:name="com.example.abe.ui.settings.SettingsFragment" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0016c8555aad7b0adf46b96f07a24acfc83d5bb0..e5e09b1293522859c5ffd5fe710e064d1f791d1a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,8 @@ poiOoxml = "5.2.5" activity = "1.8.0" playServicesLocation = "21.2.0" camerax = "1.4.0-alpha04" +glide = "4.12.0" +okhttp = "4.9.0" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -53,6 +55,10 @@ play-services-location = { group = "com.google.android.gms", name = "play-servic camera-core = { module = "androidx.camera:camera-camera2", version.ref = "camerax" } camera-lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camerax" } camera-view = { module = "androidx.camera:camera-view", version.ref = "camerax" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glideCompiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }