diff --git a/app/src/main/java/com/example/nerbos/MainActivity.kt b/app/src/main/java/com/example/nerbos/MainActivity.kt index 6d8eeeff1eb15860623002c357be67c0256fa461..7e20fe6bb5a6d5518bf8d2ec0cc7b3ee1ed679c2 100644 --- a/app/src/main/java/com/example/nerbos/MainActivity.kt +++ b/app/src/main/java/com/example/nerbos/MainActivity.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import com.example.nerbos.databinding.ActivityMainBinding import com.example.nerbos.fragments.scan.ScanFragment +import com.example.nerbos.fragments.twibbon.TwibbonFragment import com.example.nerbos.fragments.statistic.StatisticFragment import com.example.nerbos.fragments.transaction.TransactionFragment import com.example.nerbos.fragments.user.UserFragment @@ -18,12 +19,14 @@ class MainActivity : AppCompatActivity() { private val fragments = mapOf( R.id.transaction to TransactionFragment(), R.id.scan to ScanFragment(), + R.id.twibbon to TwibbonFragment(), R.id.statistic to StatisticFragment(), R.id.user to UserFragment() ) private val fragmentTitles = mapOf( R.id.transaction to R.string.navbar_transaction, R.id.scan to R.string.navbar_scan, + R.id.twibbon to R.string.navbar_twibbon, R.id.statistic to R.string.navbar_statistic, R.id.user to R.string.navbar_user ) diff --git a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt index f4c1d9c548598f2a0ce589070244d120c0c90651..81bedaa9f664cf07f7ec62551c6b819aba39ffb5 100644 --- a/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt +++ b/app/src/main/java/com/example/nerbos/fragments/scan/ScanFragment.kt @@ -2,7 +2,6 @@ package com.example.nerbos.fragments.scan import android.Manifest import android.app.Activity -import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap @@ -68,23 +67,11 @@ class ScanFragment : Fragment() { private lateinit var fusedLocationProviderClient: FusedLocationProviderClient private lateinit var geocoder: Geocoder private var networkManagerService: NetworkManagerService = NetworkManagerService() - private var fragmentContext: Context? = null - private val requestCameraPermissionCode = Manifest.permission.CAMERA private val uploadURL: String by lazy { requireContext().getString(R.string.backend_api_scan) } - override fun onAttach(context: Context) { - super.onAttach(context) - fragmentContext = context - } - - override fun onDetach() { - super.onDetach() - fragmentContext = null - } - override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? diff --git a/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt b/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..1cd5e7ecadeb8b0dd4bae53fe2eae5887b9444f4 --- /dev/null +++ b/app/src/main/java/com/example/nerbos/fragments/twibbon/TwibbonFragment.kt @@ -0,0 +1,349 @@ +package com.example.nerbos.fragments.twibbon + +import android.Manifest +import android.content.ContentValues +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Matrix +import android.graphics.Paint +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageButton +import android.widget.ImageView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +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.content.ContextCompat +import androidx.core.net.toUri +import androidx.exifinterface.media.ExifInterface +import androidx.fragment.app.Fragment +import com.example.nerbos.R +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import java.io.File +import java.io.IOException +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +class TwibbonFragment : Fragment() { + + private lateinit var previewView: PreviewView + private lateinit var cameraExecutor: ExecutorService + private lateinit var imageCapture: ImageCapture + private lateinit var twibbonImageView: ImageView + private var currentTwibbonIndex = 0 + private val twibbonTemplates = arrayOf( + R.drawable.twibbon_template_1, + R.drawable.twibbon_template_2, + R.drawable.twibbon_template_3, + R.drawable.twibbon_template_4, + R.drawable.twibbon_template_5 + ) + private val requestCameraPermissionCode = Manifest.permission.CAMERA + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + // Inflate the layout for this fragment + val view = inflater.inflate(R.layout.fragment_twibbon, container, false) + previewView = view.findViewById(R.id.previewView) + twibbonImageView = view.findViewById(R.id.twibbonImageView) + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + cameraExecutor = Executors.newSingleThreadExecutor() + + if (allPermissionsGranted()) { + startCamera() + } else { + requestCameraPermission.launch(requestCameraPermissionCode) + } + + view.findViewById<ImageButton>(R.id.captureButton).setOnClickListener { + takePicture() + } + + view.findViewById<ImageButton>(R.id.leftButton).setOnClickListener { + currentTwibbonIndex = (currentTwibbonIndex - 1 + twibbonTemplates.size) % twibbonTemplates.size + updateTwibbonTemplate() + } + + view.findViewById<ImageButton>(R.id.rightButton).setOnClickListener { + currentTwibbonIndex = (currentTwibbonIndex + 1) % twibbonTemplates.size + updateTwibbonTemplate() + } + } + + private fun updateTwibbonTemplate() { + twibbonImageView.setImageResource(twibbonTemplates[currentTwibbonIndex]) + } + + private fun allPermissionsGranted() = ContextCompat.checkSelfPermission( + requireContext(), + Manifest.permission.CAMERA + ) == PackageManager.PERMISSION_GRANTED + + private fun startCamera() { + val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext()) + + cameraProviderFuture.addListener({ + val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() + + val preview = Preview.Builder() + .build() + .also { + it.setSurfaceProvider(previewView.surfaceProvider) + } + + imageCapture = ImageCapture.Builder().build() + + val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA + + try { + cameraProvider.unbindAll() + cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture) + } catch (e: Exception) { + e.printStackTrace() + } + + }, ContextCompat.getMainExecutor(requireContext())) + } + + private fun takePicture() { + val imageCapture = imageCapture + + // Create output file to hold the captured image + val externalFilesDirs = requireContext().getExternalFilesDirs(null) + val photoFile = File(externalFilesDirs.firstOrNull(), "${System.currentTimeMillis()}.jpg") + + // Setup image capture metadata + val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build() + + // Capture the image + imageCapture.takePicture( + outputOptions, + ContextCompat.getMainExecutor(requireContext()), + object : ImageCapture.OnImageSavedCallback { + override fun onError(exc: ImageCaptureException) { + Toast.makeText(requireContext(), "Error capturing image: ${exc.message}", Toast.LENGTH_SHORT).show() + } + + override fun onImageSaved(output: ImageCapture.OutputFileResults) { + // Image captured successfully, read orientation metadata + val savedUri = photoFile.toUri() + val cameraBitmap = BitmapFactory.decodeFile(savedUri.path) + val rotatedCameraBitmap = savedUri.path?.let { + rotateImageIfRequired(cameraBitmap, + it + ) + } + val twibbonBitmap = BitmapFactory.decodeResource(resources, twibbonTemplates[currentTwibbonIndex]) + + val combinedBitmap = overlayBitmap(rotatedCameraBitmap!!, twibbonBitmap) + showImagePopup(combinedBitmap) + } + }) + } + + private fun overlayBitmap(cameraBitmap: Bitmap, twibbonBitmap: Bitmap): Bitmap { + // Determine the side length of the square image + val sideLength = minOf(cameraBitmap.width, cameraBitmap.height) + + // Calculate the position to draw the twibbon image to center it + val left = (cameraBitmap.width - sideLength) / 2 + val top = (cameraBitmap.height - sideLength) / 2 + + // Crop or resize the camera image to make it square + val croppedCameraBitmap = Bitmap.createBitmap(cameraBitmap, 0, 0, sideLength, sideLength) + + // Resize the twibbon image to match the dimensions of the cropped camera image + val resizedTwibbonBitmap = Bitmap.createScaledBitmap(twibbonBitmap, sideLength, sideLength, true) + + // Create a new bitmap with the combined images + val combinedBitmap = Bitmap.createBitmap(cameraBitmap.width, cameraBitmap.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(combinedBitmap) + val paint = Paint(Paint.FILTER_BITMAP_FLAG) + + // Draw the cropped camera image onto the canvas + canvas.drawBitmap(croppedCameraBitmap, Matrix(), paint) + + // Draw the resized twibbon image onto the canvas at the calculated position + canvas.drawBitmap(resizedTwibbonBitmap, left.toFloat(), top.toFloat(), paint) + + return combinedBitmap + } + + private fun rotateImageIfRequired(bitmap: Bitmap, path: String): Bitmap { + val ei = ExifInterface(path) + val orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + + return when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, 90f) + ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, 180f) + ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, 270f) + else -> bitmap + } + } + + private fun rotateImage(source: Bitmap, angle: Float): Bitmap { + val matrix = Matrix() + matrix.postRotate(angle) + return Bitmap.createBitmap(source, 0, 0, source.width, source.height, matrix, true) + } + + private fun showImagePopup(imageBitmap: Bitmap?) { + if (imageBitmap != null) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Save this picture?") + .setPositiveButton("Yes") { dialog, _ -> + showFilenameInputDialog(imageBitmap) + dialog.dismiss() + } + .setNegativeButton("No") { dialog, _ -> + dialog.dismiss() + } + .setView(ImageView(requireContext()).apply { + setImageBitmap(imageBitmap) + adjustViewBounds = true + }) + .show() + .apply { + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + } + } else { + showToastOnUIThread("Failed to load image") + } + } + + private fun showFilenameInputDialog(imageBitmap: Bitmap) { + val inputView = LayoutInflater.from(requireContext()).inflate(R.layout.twibbon_filename, null) + val filenameEditText = inputView.findViewById<EditText>(R.id.filenameInput) + + MaterialAlertDialogBuilder(requireContext()) + .setTitle("Save Image") + .setView(inputView) + .setPositiveButton("Save") { dialog, _ -> + val filename = filenameEditText.text.toString().trim() + if (filename.isNotEmpty()) { + saveImage(imageBitmap, filename) + } else { + Toast.makeText(requireContext(), "Please enter a filename", Toast.LENGTH_SHORT).show() + showFilenameInputDialog(imageBitmap) + } + dialog.dismiss() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .show() + .apply { + getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor( + ContextCompat.getColor( + requireContext(), + R.color.white + ) + ) + } + } + + private fun saveImage(imageBitmap: Bitmap, filename: String) { + // Crop the image to a square (1:1 aspect ratio) + val croppedImage = if (imageBitmap.width >= imageBitmap.height) { + Bitmap.createBitmap( + imageBitmap, + (imageBitmap.width - imageBitmap.height) / 2, + 0, + imageBitmap.height, + imageBitmap.height + ) + } else { + Bitmap.createBitmap( + imageBitmap, + 0, + (imageBitmap.height - imageBitmap.width) / 2, + imageBitmap.width, + imageBitmap.width + ) + } + + // Prepare values for saving the image + val contentValues = ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, "$filename.jpg") + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + } + + // Insert the image into MediaStore and get its URI + val resolver = requireContext().contentResolver + val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + try { + // Save the image to the specified URI + if (uri != null) { + resolver.openOutputStream(uri)?.use { outputStream -> + croppedImage.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + outputStream.close() + Toast.makeText(requireContext(), "Twibbon saved successfully", Toast.LENGTH_SHORT).show() + } + } else { + showToastOnUIThread("Failed to save image") + } + } catch (e: IOException) { + e.printStackTrace() + showToastOnUIThread("Failed to save image") + } + } + + private fun showToastOnUIThread(message: String) { + Handler(Looper.getMainLooper()).post { + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + } + } + + private val requestCameraPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + startCamera() + } else { + // Handle the case where the user denied the permission + Toast.makeText(requireContext(), "Camera permission denied", Toast.LENGTH_SHORT).show() + } + } + + override fun onDestroy() { + super.onDestroy() + cameraExecutor.shutdown() + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/twibbon_filename.xml b/app/src/main/res/layout/twibbon_filename.xml new file mode 100644 index 0000000000000000000000000000000000000000..b149e2a22d81053dba6a40198bd65da98ffa4511 --- /dev/null +++ b/app/src/main/res/layout/twibbon_filename.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:padding="16dp"> + + <EditText + android:id="@+id/filenameInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/name" + android:inputType="text" + android:autofillHints="Twibbon Filename" /> + +</LinearLayout> \ No newline at end of file