diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c5b147c31e0d5c4a7aea53b41936dbccd4f5dcf8..b17c560ce87d64a0eac87478dfe9988f90606a04 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -58,6 +58,7 @@ dependencies { implementation(libs.camera.lifecycle) implementation(libs.camera.view) implementation(libs.glide) + implementation(libs.androidx.exifinterface) annotationProcessor(libs.glideCompiler) implementation(libs.androidx.activity) implementation(libs.play.services.location) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3af3ac6c9e52e8c623da9d2cdec450a3779b2a9..155886d1acf6d0f5a8ddb5871f1ddf817f0f86a2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -5,7 +5,6 @@ <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.WRITE_EXTERNAL_STORAGE" /> <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" /> diff --git a/app/src/main/java/com/example/abe/MainActivity.kt b/app/src/main/java/com/example/abe/MainActivity.kt index 1c2c332d9497191ed587f3585c6f5a2da6ce1b49..a9a6ee06a40f9c83ac564cb8f1d485c27eff4037 100644 --- a/app/src/main/java/com/example/abe/MainActivity.kt +++ b/app/src/main/java/com/example/abe/MainActivity.kt @@ -6,6 +6,7 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View import android.widget.Toast @@ -93,6 +94,7 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR preferenceDataStoreHelper = PreferenceDataStoreHelper(applicationContext) binding = ActivityMainBinding.inflate(layoutInflater) @@ -107,6 +109,7 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD R.id.navigation_transactions, R.id.navigation_graph, R.id.navigation_settings, + R.id.navigation_twibbon, R.id.navigation_scan ) ) @@ -116,6 +119,9 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD navController.addOnDestinationChangedListener { _, destination, _ -> if (destination.id == R.id.navigation_form_transaction) navView.visibility = View.GONE else navView.visibility = View.VISIBLE + + if (destination.id == R.id.navigation_graph) requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR + else requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } LocalBroadcastManager.getInstance(this).registerReceiver(br, filter) diff --git a/app/src/main/java/com/example/abe/MainActivityViewModel.kt b/app/src/main/java/com/example/abe/MainActivityViewModel.kt index d65253b4cee4f5aa229e11536afe552a8103f4db..afc842dc687d9c585fc6be5a53733c739eeb6b47 100644 --- a/app/src/main/java/com/example/abe/MainActivityViewModel.kt +++ b/app/src/main/java/com/example/abe/MainActivityViewModel.kt @@ -47,7 +47,7 @@ class MainActivityViewModel(private val transactionRepository: TransactionReposi suspend fun createEmailIntent(context: Context, user: String): Intent { clearExportCacheFiles(context) - val newFile = File(context.externalCacheDir, if (newExcelFormat) "transaction-export.xlsx" else "transaction-export.xls") + val newFile = File(context.cacheDir, if (newExcelFormat) "transaction-export.xlsx" else "transaction-export.xls") val contentUri = FileProvider.getUriForFile(context, "com.example.abe.fileprovider", newFile) exportTransactionsToExcel(context.contentResolver, contentUri, user) @@ -66,7 +66,7 @@ class MainActivityViewModel(private val transactionRepository: TransactionReposi } fun clearExportCacheFiles(context: Context) { - context.externalCacheDir?.apply { + context.cacheDir?.apply { val files = listFiles() ?: emptyArray() files.forEach { file -> if (file.name.startsWith("export")) diff --git a/app/src/main/java/com/example/abe/data/TransactionDAO.kt b/app/src/main/java/com/example/abe/data/TransactionDAO.kt index 3dda96965e6c03661ab3d23607972eefa1c26989..51a67245beff268dcfaac8253549c7f41922f448 100644 --- a/app/src/main/java/com/example/abe/data/TransactionDAO.kt +++ b/app/src/main/java/com/example/abe/data/TransactionDAO.kt @@ -22,10 +22,10 @@ interface TransactionDAO { @Query("DELETE FROM transactions") suspend fun deleteAll() - @Query("SELECT * FROM transactions WHERE email = :email") + @Query("SELECT * FROM transactions WHERE email = :email ORDER BY timestamp DESC") fun getAllObservable(vararg email: String): LiveData<List<Transaction>> - @Query("SELECT * FROM transactions WHERE email = :email") + @Query("SELECT * FROM transactions WHERE email = :email ORDER BY timestamp DESC") suspend fun getAll(vararg email: String): List<Transaction> @Query("SELECT * FROM transactions WHERE id = :id") 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 bd9b8605b78f7b6f4714a7d04256661d3cb9bc53..21536640fdf2b360b5306ab1bbbe4817ef1b918c 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 @@ -126,7 +126,6 @@ class Retrofit { } override fun onFailure(call: Call<ItemsRoot>, t: Throwable) { - Log.d("ABE-PHO", "Failed to send request: " + t.message) callback.onFailure("Failed to send photo") } }) diff --git a/app/src/main/java/com/example/abe/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/abe/ui/dashboard/DashboardFragment.kt deleted file mode 100644 index 3351e83a456c4a58c40e979e850d5e1650c81ece..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/abe/ui/dashboard/DashboardFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.abe.ui.dashboard - -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.FragmentDashboardBinding - -class DashboardFragment : Fragment() { - - private var _binding: FragmentDashboardBinding? = 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 dashboardViewModel = - ViewModelProvider(this).get(DashboardViewModel::class.java) - - _binding = FragmentDashboardBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textDashboard - dashboardViewModel.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/dashboard/DashboardViewModel.kt b/app/src/main/java/com/example/abe/ui/dashboard/DashboardViewModel.kt deleted file mode 100644 index 592e6697da608c2eec0cc5e3380e899d68aa99b2..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/abe/ui/dashboard/DashboardViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.abe.ui.dashboard - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class DashboardViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is dashboard Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt b/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt index c7e3ae5f92093e3a48a318dee8cd78b0fbe52f8a..96588b63e6f5f7d8acba26b0aad4f015e504d347 100644 --- a/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt +++ b/app/src/main/java/com/example/abe/ui/form_transaction/FormTransaction.kt @@ -46,7 +46,7 @@ class FormTransaction : Fragment() { private lateinit var fusedLocationProviderClient: FusedLocationProviderClient - private val permissionId = 5 + private var isNewTrx = true private val viewModel: FormTransactionViewModel by viewModels { FormTransactionViewModelFactory((activity?.application as ABEApplication).repository) @@ -107,6 +107,7 @@ class FormTransaction : Fragment() { if (args.containsKey("idx-id")) { val trxId = args.getInt("idx-id") displayTrx(trxId) + isNewTrx = false } else if (args.containsKey("random_amount")) { viewModel.setRandomAmount(args.getInt("random_amount")) useNewTrxLayout() @@ -271,7 +272,7 @@ class FormTransaction : Fragment() { } if (granted) { getCurrentLocation() - } else { + } else if (isNewTrx) { val defaultLatitude = -6.892382 val defaultLongitude = 107.608352 Toast.makeText(requireActivity(), "Location set to default", Toast.LENGTH_SHORT).show() @@ -307,13 +308,13 @@ class FormTransaction : Fragment() { val location: Location? = task.result if (location != null) { setLocation(location.latitude, location.longitude) - } else { + } else if (isNewTrx) { Toast.makeText(requireActivity(), "Location set to default", Toast.LENGTH_SHORT) .show() setLocation(defaultLatitude, defaultLongitude) } } - } else { + } else if (isNewTrx) { Toast.makeText(requireActivity(), "Location set to default", Toast.LENGTH_SHORT).show() setLocation(defaultLatitude, defaultLongitude) } diff --git a/app/src/main/java/com/example/abe/ui/home/HomeFragment.kt b/app/src/main/java/com/example/abe/ui/home/HomeFragment.kt deleted file mode 100644 index bfa9c99f55e6d26ea3b044bea9ede6af92bf3be8..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/abe/ui/home/HomeFragment.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.abe.ui.home - -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.FragmentHomeBinding - -class HomeFragment : Fragment() { - - private var _binding: FragmentHomeBinding? = 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 homeViewModel = - ViewModelProvider(this).get(HomeViewModel::class.java) - - _binding = FragmentHomeBinding.inflate(inflater, container, false) - val root: View = binding.root - - val textView: TextView = binding.textHome - homeViewModel.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/home/HomeViewModel.kt b/app/src/main/java/com/example/abe/ui/home/HomeViewModel.kt deleted file mode 100644 index c17bcd97d032b49640948b6b249adcfbdc713c86..0000000000000000000000000000000000000000 --- a/app/src/main/java/com/example/abe/ui/home/HomeViewModel.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.abe.ui.home - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel - -class HomeViewModel : ViewModel() { - - private val _text = MutableLiveData<String>().apply { - value = "This is home Fragment" - } - val text: LiveData<String> = _text -} \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt b/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt index b562f9192cfc49650f088827b0d7610756297e22..e8869b740bac22c18b48d1915e2bfe56fe0819c0 100644 --- a/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt +++ b/app/src/main/java/com/example/abe/ui/login/LoginActivity.kt @@ -1,6 +1,7 @@ package com.example.abe.ui.login import android.content.Intent +import android.content.pm.ActivityInfo import android.os.Bundle import android.util.Patterns import android.view.View @@ -65,6 +66,7 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { super.onCreate(savedInstanceState) binding = ActivityLoginBinding.inflate(layoutInflater) + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT supportActionBar?.hide() val view = binding.root 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 index c3a4f37f34228a5bfeee7ee3a018c6f78a7bacdb..7988086650124b35ef44585061d22ff331e78b83 100644 --- a/app/src/main/java/com/example/abe/ui/scanner/ScannerFragment.kt +++ b/app/src/main/java/com/example/abe/ui/scanner/ScannerFragment.kt @@ -64,13 +64,14 @@ class ScannerFragment : Fragment(), UploadResultCallback { 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 var isProcessingPhoto = false + private val viewModel: ScannerViewModel by viewModels { ScannerViewModelFactory((activity?.application as ABEApplication).repository) } @@ -118,6 +119,7 @@ class ScannerFragment : Fragment(), UploadResultCallback { } else { val msg = "Failed to fetch image from gallery" Toast.makeText(requireContext(), msg, Toast.LENGTH_SHORT).show() + isProcessingPhoto = false } } } @@ -137,12 +139,21 @@ class ScannerFragment : Fragment(), UploadResultCallback { } lifecycleScope.launch { - user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( - PreferenceDataStoreConstants.USER,"") + user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.USER, "" + ) } binding.captureButton.setOnClickListener { - takePicture() + if (cameraPermissionGranted()) { + takePicture() + } else { + Toast.makeText( + requireContext(), + "Please allow camera to take photos", + Toast.LENGTH_SHORT + ).show() + } } binding.galleryPreviewButton.setOnClickListener { @@ -190,13 +201,17 @@ class ScannerFragment : Fragment(), UploadResultCallback { private fun attemptUpload(imageFile: File) { lifecycleScope.launch { val retrofit = Retrofit() - val token = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference(PreferenceDataStoreConstants.TOKEN, "") + val token = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.TOKEN, + "" + ) retrofit.upload(token, imageFile, this@ScannerFragment) } } private fun showPreviewDialog(imageUri: Uri) { val dialog = Dialog(requireContext()).apply { + setCancelable(false) setContentView(R.layout.dialog_image_preview) window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) } @@ -212,23 +227,25 @@ class ScannerFragment : Fragment(), UploadResultCallback { confirmButton.setOnClickListener { val activity = activity as MainActivity - if(!isConnected(activity.getNetworkState())) { + if (!isConnected(activity.getNetworkState())) { dialog.dismiss() binding.scanLayout.visibility = View.GONE binding.noNetworkLayout.visibility = View.VISIBLE + isProcessingPhoto = false } else { - val filePath = imageUri.path - if (filePath != null) { - val imageFile = File(filePath) - attemptUpload(imageFile) + 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_LONG).show() + val msg = "Uploading photo, please wait" + Toast.makeText(requireContext(), msg, Toast.LENGTH_LONG).show() - dialog.dismiss() - } else { - dialog.dismiss() - } + dialog.dismiss() + } else { + isProcessingPhoto = false + dialog.dismiss() + } } } @@ -239,12 +256,22 @@ class ScannerFragment : Fragment(), UploadResultCallback { cancelButton.setOnClickListener { dialog.dismiss() + isProcessingPhoto = false } dialog.show() } private fun takePicture() { + if (isProcessingPhoto) { + Toast.makeText( + requireContext(), + "Unable to take picture, processing previous image", + Toast.LENGTH_SHORT + ).show() + return + } + val imageCapture = imageCapture val photoFile = File( @@ -254,6 +281,7 @@ class ScannerFragment : Fragment(), UploadResultCallback { val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + isProcessingPhoto = true imageCapture.takePicture( outputOptions, ContextCompat.getMainExecutor(requireContext()), @@ -261,6 +289,7 @@ class ScannerFragment : Fragment(), UploadResultCallback { override fun onError(exc: ImageCaptureException) { Toast.makeText(requireContext(), "Photo capture failed", Toast.LENGTH_SHORT) .show() + isProcessingPhoto = false } override fun onImageSaved(output: ImageCapture.OutputFileResults) { @@ -274,7 +303,6 @@ class ScannerFragment : Fragment(), UploadResultCallback { private fun setLocationAsDefault() { latitude = DEFAULT_LATITUDE longitude = DEFAULT_LONGINTUDE - useDefaultLocation = true } @SuppressLint("MissingPermission") @@ -295,6 +323,7 @@ class ScannerFragment : Fragment(), UploadResultCallback { } } else { setLocationAsDefault() + insertItems() } } @@ -351,7 +380,7 @@ class ScannerFragment : Fragment(), UploadResultCallback { 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" + if (locationList.size > 0) (locationList[0].getAddressLine(0)) else "Unknown location" uploadResponse?.items?.items?.forEach { item -> viewModel.insertTransaction(user, item, latitude, longitude, location) @@ -361,16 +390,28 @@ class ScannerFragment : Fragment(), UploadResultCallback { findNavController().navigate(R.id.action_navigation_scanner_to_navigation_transactions) uploadResponse = null + isProcessingPhoto = false } override fun onFailure(errorMessage: String) { Toast.makeText(requireContext(), "Upload failed", Toast.LENGTH_SHORT).show() Log.e("ABE-PHO", errorMessage) + isProcessingPhoto = false } private fun openGallery() { + if (isProcessingPhoto) { + Toast.makeText( + requireContext(), + "Unable choose image, processing previous image", + Toast.LENGTH_SHORT + ).show() + return + } + val intent = Intent(Intent.ACTION_PICK) intent.type = "image/*" + isProcessingPhoto = true openGalleryLauncher.launch(intent) } diff --git a/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..ffc798f88d346c698bf7e8ee551b7f6dd55dd0e4 --- /dev/null +++ b/app/src/main/java/com/example/abe/ui/twibbon/TwibbonFragment.kt @@ -0,0 +1,316 @@ +package com.example.abe.ui.twibbon + +import android.Manifest +import android.app.Dialog +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Bitmap.CompressFormat +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Matrix +import android.graphics.drawable.ColorDrawable +import android.net.Uri +import android.os.Bundle +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.core.content.ContextCompat +import androidx.exifinterface.media.ExifInterface +import androidx.fragment.app.Fragment +import com.bumptech.glide.Glide +import com.example.abe.R +import com.example.abe.databinding.FragmentTwibbonBinding +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.net.URI +import java.text.SimpleDateFormat +import java.util.Locale +import java.util.concurrent.ExecutorService + + +class TwibbonFragment : Fragment() { + private var _binding: FragmentTwibbonBinding? = null + private val binding get() = _binding!! + + private var imageCapture: ImageCapture? = null + + private var isProcessingPhoto = false + + companion object { + private const val TAG = "ABE-TWB" + private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentTwibbonBinding.inflate(inflater, container, false) + + if (cameraPermissionsGranted()) { + startCamera() + } else { + requestCameraPermissions() + } + + binding.btnCaptureTwibbon.setOnClickListener { + if (cameraPermissionsGranted()) { + previewTwibbon() + } else { + Toast.makeText(requireContext(), "Please allow camera to take photos", Toast.LENGTH_SHORT).show() + } + } + + 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(binding.prvTwibbon.surfaceProvider) + } + imageCapture = ImageCapture.Builder().build() + + val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA + + try { + cameraProvider.unbindAll() + + cameraProvider.bindToLifecycle( + this, cameraSelector, preview, imageCapture + ) + + } catch (exc: Exception) { + Log.e(TAG, "Use case binding failed", exc) + } + + }, ContextCompat.getMainExecutor(requireContext())) + } + + private val cameraPermissionsLauncher = + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) + { isGranted -> + if (isGranted) { + startCamera() + } else { + Toast.makeText( + requireContext(), + "Please allow camera to use Twibbon", + Toast.LENGTH_SHORT + ).show() + } + } + + private fun previewTwibbon() { + if (isProcessingPhoto) { + Toast.makeText(requireContext(), "Unable to take picture, processing previous image", Toast.LENGTH_SHORT).show() + return + } + + isProcessingPhoto = true + Toast.makeText(requireContext(), "Generating twibbon", Toast.LENGTH_SHORT).show() + val imageCapture = imageCapture ?: return + deletePreviousTwibbons() + + val photoFile = File( + requireContext().cacheDir, + "twibbon_${ + 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() + isProcessingPhoto = false + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + val savedUri = Uri.fromFile(photoFile) + overlayTwibbonToImage(savedUri) + showPreviewDialog(savedUri) + } + } + ) + } + + private fun overlayTwibbonToImage(imageUri: Uri) { + try { + val exifIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return + val exif = ExifInterface(exifIms) + val orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, 1) + + val overlayBitmap = BitmapFactory.decodeResource( + requireContext().resources, + R.drawable.img_default_twibbon + ) + + val bitmapIms = requireActivity().contentResolver.openInputStream(imageUri) ?: return + val originalPhotoBitmap = BitmapFactory.decodeStream(bitmapIms) + val rotationMatrix = Matrix() + + when (orientation) { + 6 -> { + rotationMatrix.postRotate(90f) + } + + 3 -> { + rotationMatrix.postRotate(180f) + } + + 8 -> { + rotationMatrix.postRotate(270f) + } + } + + val rotatedBitmap = Bitmap.createBitmap( + originalPhotoBitmap, + 0, + 0, + originalPhotoBitmap.width, + originalPhotoBitmap.height, + rotationMatrix, + true + ) + + val flipMatrix = Matrix().apply { + postScale( + -1f, + 1f, + rotatedBitmap.width.toFloat() / 2, + rotatedBitmap.height.toFloat() + ) + } + val flippedBitmap = Bitmap.createBitmap( + rotatedBitmap, + 0, + 0, + rotatedBitmap.width, + rotatedBitmap.height, + flipMatrix, + true + ) + + val scaleMatrix = Matrix().apply { + val scale = overlayBitmap.width.toFloat() / flippedBitmap.width.toFloat() + postScale( + scale, + scale, + flippedBitmap.width.toFloat(), + flippedBitmap.height.toFloat() + ) + } + + val photoBitmap = Bitmap.createBitmap( + flippedBitmap, + 0, + 0, + flippedBitmap.width, + flippedBitmap.height, + scaleMatrix, + true + ) + val translateMatrix = Matrix().apply { + postTranslate( + 0f, + (overlayBitmap.height.toFloat() - photoBitmap.height.toFloat()) / 2f + ) + } + + val resultBitmap = + Bitmap.createBitmap( + overlayBitmap.width, + overlayBitmap.height, + overlayBitmap.getConfig() + ) + val canvas = Canvas(resultBitmap) + canvas.drawBitmap(photoBitmap, translateMatrix, null) + canvas.drawBitmap(overlayBitmap, Matrix(), null) + + val bos = ByteArrayOutputStream() + resultBitmap.compress(CompressFormat.JPEG, 100, bos) + val bitmapData = bos.toByteArray() + + val f = File(URI(imageUri.toString())) + + val fos = FileOutputStream(f) + fos.write(bitmapData) + fos.flush() + fos.close() + + } catch (e: FileNotFoundException) { + e.printStackTrace() + } + } + + private fun deletePreviousTwibbons() { + requireContext().cacheDir?.apply { + val files = listFiles() ?: emptyArray() + files.forEach { file -> + if (file.name.startsWith("twibbon")) + file.delete() + } + } + } + + private fun showPreviewDialog(imageUri: Uri) { + val dialog = Dialog(requireContext()).apply { + setCancelable(false) + setContentView(R.layout.dialog_twibbon_preview) + window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + } + + val imageView = dialog.findViewById<ImageView>(R.id.ivTwibbonPreview) + + Glide.with(requireContext()) + .load(imageUri) + .into(imageView) + + val closeButton = dialog.findViewById<Button>(R.id.btnCloseTwibbon) + + closeButton.setOnClickListener { + isProcessingPhoto = false + dialog.dismiss() + } + + dialog.show() + } + + private fun cameraPermissionsGranted() = ContextCompat.checkSelfPermission( + requireContext(), Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + private fun requestCameraPermissions() { + cameraPermissionsLauncher.launch(Manifest.permission.CAMERA) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_twibbon.xml b/app/src/main/res/drawable/ic_twibbon.xml new file mode 100644 index 0000000000000000000000000000000000000000..00c4042dc5a00b86dd4de79e9fe130e45c6698f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_twibbon.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="@color/primary" + android:pathData="M520,840L200,840q-33,0 -56.5,-23.5T120,760v-320h400v400ZM160,760h320L376,620l-76,100 -56,-74 -84,114ZM200,200h-80q0,-33 23.5,-56.5T200,120v80ZM280,200v-80h80v80h-80ZM440,200v-80h80v80h-80ZM600,200v-80h80v80h-80ZM600,840v-80h80v80h-80ZM760,200v-80q33,0 56.5,23.5T840,200h-80ZM120,360v-80h80v80h-80ZM760,760h80q0,33 -23.5,56.5T760,840v-80ZM760,680v-80h80v80h-80ZM760,520v-80h80v80h-80ZM760,360v-80h80v80h-80Z"/> +</vector> diff --git a/app/src/main/res/drawable/img_default_twibbon.png b/app/src/main/res/drawable/img_default_twibbon.png new file mode 100644 index 0000000000000000000000000000000000000000..59df76b7525c37668922be2df1fcf8606a543ade Binary files /dev/null and b/app/src/main/res/drawable/img_default_twibbon.png differ diff --git a/app/src/main/res/layout/dialog_twibbon_preview.xml b/app/src/main/res/layout/dialog_twibbon_preview.xml new file mode 100644 index 0000000000000000000000000000000000000000..16fe1878211715773e9851e30520d1eea9668667 --- /dev/null +++ b/app/src/main/res/layout/dialog_twibbon_preview.xml @@ -0,0 +1,32 @@ +<?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"> + + <ImageView + android:id="@+id/ivTwibbonPreview" + 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/btnCloseTwibbon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="48dp" + android:text="Close Preview" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintHorizontal_chainStyle="spread" + app:layout_constraintStart_toStartOf="parent" /> + + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_dashboard.xml b/app/src/main/res/layout/fragment_dashboard.xml deleted file mode 100644 index 166ab0e9e603c1f230a7b9514d293b963ab2309e..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_dashboard.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.dashboard.DashboardFragment"> - - <TextView - android:id="@+id/text_dashboard" - 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_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index f3d9b08ffe6101e25c77c5fae7e28bb5dfa11fbd..0000000000000000000000000000000000000000 --- a/app/src/main/res/layout/fragment_home.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.home.HomeFragment"> - - <TextView - android:id="@+id/text_home" - 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_twibbon.xml b/app/src/main/res/layout/fragment_twibbon.xml new file mode 100644 index 0000000000000000000000000000000000000000..acddcc0ba9259ca25033bb9061177eb4c30d82ae --- /dev/null +++ b/app/src/main/res/layout/fragment_twibbon.xml @@ -0,0 +1,72 @@ +<?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.twibbon.TwibbonFragment"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:background="@color/grayLight" + app:layout_constraintBottom_toTopOf="@+id/clPreviewBottomBar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.camera.view.PreviewView + android:id="@+id/prvTwibbon" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1:1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" /> + + <ImageView + android:id="@+id/ivTwibbonOverlay" + android:layout_width="match_parent" + android:layout_height="0dp" + android:src="@drawable/img_default_twibbon" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintDimensionRatio="1:1" + 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" + android:contentDescription="@string/twibbon_overlay" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/clPreviewBottomBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@color/primaryActive" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> + + <Button + android:id="@+id/btnCaptureTwibbon" + 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" /> + + + </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 8f9ae85695d9e5c3c51856393458b5ab6a780966..3d0ae195ab8db9a35331632b3b0012dccadf4f32 100644 --- a/app/src/main/res/menu/bottom_nav_menu.xml +++ b/app/src/main/res/menu/bottom_nav_menu.xml @@ -6,11 +6,18 @@ android:icon="@drawable/ic_transactions" android:title="@string/transaction" /> + <item + android:id="@+id/navigation_twibbon" + android:icon="@drawable/ic_twibbon" + android:title="Twibbon" + /> + <item android:id="@+id/navigation_scan" android:icon="@drawable/ic_scan" android:title="@string/scan" /> + <item android:id="@+id/navigation_graph" android:icon="@drawable/ic_graph" diff --git a/app/src/main/res/navigation/mobile_navigation.xml b/app/src/main/res/navigation/mobile_navigation.xml index 73076dc2852be4ecf20540e7b04b98d2bb56190f..89e74cf9cc6ead417575003857e1fe66867382fe 100644 --- a/app/src/main/res/navigation/mobile_navigation.xml +++ b/app/src/main/res/navigation/mobile_navigation.xml @@ -57,6 +57,13 @@ app:destination="@id/navigation_transactions" /> </fragment> + <fragment + android:id="@+id/navigation_twibbon" + android:label="Twibbon" + android:name="com.example.abe.ui.twibbon.TwibbonFragment" + tools:layout="@layout/fragment_twibbon" + /> + <!-- Example when fragment & layout is done--> <!-- <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 1f2ba06c6c06e63b342a2171d519053856eb686b..b6150e23f188544f100326d84e7e6f2c006be681 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -30,4 +30,7 @@ <item>Income</item> <item>Expenses</item> </string-array> + <!-- TODO: Remove or change this placeholder text --> + <string name="hello_blank_fragment">Hello blank fragment</string> + <string name="twibbon_overlay">Twibbon overlay</string> </resources> \ No newline at end of file diff --git a/app/src/main/res/xml/filepaths.xml b/app/src/main/res/xml/filepaths.xml index 405a83d4ee28acc51f581b1c950679f7da123423..fe2c7dc9388c4f983dc7125dff5b589bc07a1131 100644 --- a/app/src/main/res/xml/filepaths.xml +++ b/app/src/main/res/xml/filepaths.xml @@ -1,10 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <paths> - <external-cache-path + <cache-path name="exports" path="/" /> - -<!-- <cache-path--> -<!-- name="exports"--> -<!-- path="/" />--> </paths> \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5e09b1293522859c5ffd5fe710e064d1f791d1a..df9a53d24b214f92f2ee16a5dc3ea323b94d9b54 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -27,6 +27,7 @@ playServicesLocation = "21.2.0" camerax = "1.4.0-alpha04" glide = "4.12.0" okhttp = "4.9.0" +exifinterface = "1.3.7" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } @@ -59,6 +60,7 @@ 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" } +androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }