diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a2ba9de45474f1ed05f1c6bb2728fbdb9fe5a4b2..98d6324f346c31eee2617a646fcb4434c88e6b94 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -54,6 +54,8 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("androidx.security:security-crypto:1.1.0-alpha06") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1") + implementation(libs.androidx.work.runtime.ktx) implementation("androidx.compose.runtime:runtime-livedata:1.0.5") implementation("androidx.compose.runtime:runtime-rxjava2:1.0.5") testImplementation(libs.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 64e5c24018e439a5978975325a27bd63e128ff18..72f59002fb490e159c507aa07c93c11c4d305b79 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,14 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + + <uses-feature + android:name="android.hardware.camera" + android:required="false" /> + <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + <uses-permission android:name="android.permission.CAMERA" /> <application diff --git a/app/src/main/java/com/example/myapplication/LoginActivity.kt b/app/src/main/java/com/example/myapplication/LoginActivity.kt index eb150605a39b97c79bc2ab651ae5deb0018b5503..43631724f07aa70428cf4f4dc85077a6c7547f9c 100644 --- a/app/src/main/java/com/example/myapplication/LoginActivity.kt +++ b/app/src/main/java/com/example/myapplication/LoginActivity.kt @@ -9,11 +9,12 @@ import androidx.appcompat.app.AppCompatActivity import com.example.myapplication.databinding.ActivityLoginBinding import com.example.myapplication.repository.AuthRepository import com.example.myapplication.ui.login.UserViewModel +import com.example.myapplication.util.EventBus import com.example.myapplication.util.LoginListener import com.example.myapplication.util.SecretPreference import com.example.myapplication.NetworkConnection -class LoginActivity : AppCompatActivity() , LoginListener { +class LoginActivity : AppCompatActivity() { private lateinit var binding: ActivityLoginBinding private var userViewModel : UserViewModel = UserViewModel() @@ -24,7 +25,7 @@ class LoginActivity : AppCompatActivity() , LoginListener { super.onCreate(savedInstanceState) secretPreference = SecretPreference(this) - authRepository = AuthRepository(this, secretPreference) + authRepository = AuthRepository(secretPreference) binding = ActivityLoginBinding.inflate(layoutInflater) setContentView(binding.root) @@ -39,9 +40,17 @@ class LoginActivity : AppCompatActivity() , LoginListener { authRepository.loginRequest(binding.emailInput.text.toString(), binding.passwordInput.text.toString()) Log.d("Development", "Activity: Login request sent") } + + EventBus.subscribe("LOGIN_SUCCESS") { + onLoginSuccess() + } + + EventBus.subscribe("LOGIN_FAIL") { + onLoginFailure() + } } - override fun onLoginSuccess() { + fun onLoginSuccess() { userViewModel.setUser(binding.emailInput.text.toString(), binding.passwordInput.text.toString()) Log.d("Development", "Activity: Login success") val preference : SharedPreferences = getSharedPreferences("secret_shared_prefs", MODE_PRIVATE) @@ -49,7 +58,7 @@ class LoginActivity : AppCompatActivity() , LoginListener { finish() } - override fun onLoginFailure() { + fun onLoginFailure() { Log.d("Development", "Activity: Login failed") } } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/MainActivity.kt b/app/src/main/java/com/example/myapplication/MainActivity.kt index 374cf18e61144d05c4f6cf3353f4b9a0283b6bd7..0c698b0fa4960e26ebcfd5a6c288d312efd41749 100644 --- a/app/src/main/java/com/example/myapplication/MainActivity.kt +++ b/app/src/main/java/com/example/myapplication/MainActivity.kt @@ -1,11 +1,11 @@ package com.example.myapplication -import android.app.AlertDialog -import android.content.Context import android.content.Intent -import android.net.ConnectivityManager -import android.net.NetworkCapabilities +import android.net.Uri import android.os.Bundle +import android.view.LayoutInflater +import android.widget.Button +import android.widget.EditText import com.google.android.material.bottomnavigation.BottomNavigationView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.MutableLiveData @@ -14,11 +14,18 @@ import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager import com.example.myapplication.databinding.ActivityMainBinding +import com.example.myapplication.service.TokenExpiryWorker +import com.example.myapplication.util.EventBus +import com.example.myapplication.util.SecretPreference +import java.util.concurrent.TimeUnit import com.example.myapplication.ui.settings.SettingsViewModel -class MainActivity : AppCompatActivity() { +class MainActivity : AppCompatActivity(){ private lateinit var binding: ActivityMainBinding + private lateinit var secretPreference : SecretPreference private val checkConnection by lazy { CheckConnection(application) } private val connected : MutableLiveData<Boolean> = MutableLiveData(true) private lateinit var viewModelSettings: SettingsViewModel @@ -43,21 +50,14 @@ class MainActivity : AppCompatActivity() { setupActionBarWithNavController(navController, appBarConfiguration) navView.setupWithNavController(navController) - viewModelSettings = ViewModelProvider(this).get(SettingsViewModel::class.java) + val isOnline : String = if (isOnline()) "Online" else "Offline" + Log.i("Development", "Online Connectivity Status: $isOnline") - val connectionLostBuilder: AlertDialog.Builder = AlertDialog.Builder(this) - connectionLostBuilder - .setMessage("Connection lost") - .setTitle("There is no internet connection") - - if (!isOnline()) { - updateConnection(false) - } else { - updateConnection(true) - } val loginIntent = Intent(this, LoginActivity::class.java) startActivity(loginIntent) + + } override fun onResume() { @@ -85,6 +85,19 @@ class MainActivity : AppCompatActivity() { fun getConnectionStatus(): Boolean { return connected.value == true + + // CHECK TOKEN for expiry + val backgroundWork = PeriodicWorkRequestBuilder<TokenExpiryWorker>(15, TimeUnit.MINUTES).build() + WorkManager.getInstance(this).enqueue(backgroundWork) + + // event listener for token expiry + EventBus.subscribe("TOKEN_EXPIRED") { + Log.i("Development", "Token expired") + secretPreference = SecretPreference(this) + secretPreference.clearToken() + val loginIntent = Intent(this, LoginActivity::class.java) + startActivity(loginIntent) + } } private fun isOnline(): Boolean { diff --git a/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt b/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt index fa10fd0b88a21d86796bdb4df5dfde4c80c5ec6f..3d2a0819d409cfafeed7d6f485087855eccaf8c3 100644 --- a/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt +++ b/app/src/main/java/com/example/myapplication/backendconnect/BackendService.kt @@ -2,18 +2,24 @@ package com.example.myapplication.backendconnect import com.example.myapplication.model.auth.Token import com.example.myapplication.model.auth.UserCred +import com.example.myapplication.model.bill.BillResponse +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 BackendService { @POST("auth/login") - fun login(@Body userCred: UserCred): Call<Token> + fun login(@Body data: Map<String, String>): Call<Token> @POST("auth/token") - fun tokenCheck(@Body token: Token): Call<Token> + fun tokenCheck(@Header("Authorization") token : String): Call<UserCred> -// @POST("bill/upload") -// fun uploadBill(@Body bill: Bill): Call<BillResponse> + @Multipart + @POST("bill/upload") + fun uploadBill(@Header("Authorization") token: String , @Part file: MultipartBody.Part): Call<BillResponse> } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/model/auth/UserCred.kt b/app/src/main/java/com/example/myapplication/model/auth/UserCred.kt index c46eb7c1d95418954d00efb8464c9b49f265f63d..5fcf2595515bcef81bb54866d97c771c384674e5 100644 --- a/app/src/main/java/com/example/myapplication/model/auth/UserCred.kt +++ b/app/src/main/java/com/example/myapplication/model/auth/UserCred.kt @@ -3,8 +3,10 @@ package com.example.myapplication.model.auth import com.google.gson.annotations.SerializedName data class UserCred ( - @SerializedName("email") - val email: String, - @SerializedName("password") - val password: String + @SerializedName("nim") + val nim: String, + @SerializedName("iat") + val iat: String, + @SerializedName("exp") + val exp: String ) \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/model/bill/BillResponse.kt b/app/src/main/java/com/example/myapplication/model/bill/BillResponse.kt new file mode 100644 index 0000000000000000000000000000000000000000..0121273f246c13f23b88512d39803b8e0cc41247 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/model/bill/BillResponse.kt @@ -0,0 +1,22 @@ +package com.example.myapplication.model.bill + +import com.google.gson.annotations.SerializedName + +data class BillResponse ( + @SerializedName("items") + val items: Items +) { + data class Items ( + @SerializedName("items") + val items: List<Item> + ) { + data class Item ( + @SerializedName("name") + val name: String, + @SerializedName("qty") + val quantity: Int, + @SerializedName("price") + val price: Double + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt b/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt index 139f5ad9618c67254bbe83e49f8acac7f48657c7..f519451746e7fce29eb1fb827a8dc6db7669c8bd 100644 --- a/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt +++ b/app/src/main/java/com/example/myapplication/repository/AuthRepository.kt @@ -1,23 +1,32 @@ package com.example.myapplication.repository -import android.content.Context +import android.net.Uri import android.util.Log +import androidx.core.content.ContentProviderCompat.requireContext import com.example.myapplication.backendconnect.Client import com.example.myapplication.model.auth.Token import com.example.myapplication.model.auth.UserCred +import com.example.myapplication.model.bill.BillResponse +import com.example.myapplication.util.EventBus import com.example.myapplication.util.LoginListener import com.example.myapplication.util.SecretPreference +import okhttp3.MediaType +import okhttp3.MultipartBody +import okhttp3.RequestBody + import retrofit2.Call import retrofit2.Callback import retrofit2.Response +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream class AuthRepository ( - private val loginListener: LoginListener, private val secretPreference: SecretPreference) { fun loginRequest(email: String, password: String){ Log.d("Development", "Login request to backend service") - Client.connect.login(UserCred(email, password)).enqueue( + Client.connect.login(mapOf("email" to email, "password" to password)).enqueue( object : Callback<Token> { override fun onResponse(call: Call<Token>, response: Response<Token>) { Log.d("Development", "Response: ${response.body()}") @@ -29,19 +38,80 @@ class AuthRepository ( secretPreference.saveToken(token?.token ?: "") // callback the loginActivity - loginListener.onLoginSuccess() + //loginListener.onLoginSuccess() + EventBus.publish("LOGIN_SUCCESS") } else { Log.d("Development", "LOGIN FAILED, http code : ${response.code()}") - loginListener.onLoginFailure() + //loginListener.onLoginFailure() + EventBus.publish("LOGIN_FAIL") } } override fun onFailure(call: Call<Token>, t: Throwable) { Log.d("Development", "LOGIN FAILED, error on delivery : ${t.message}") - loginListener.onLoginFailure() +// loginListener.onLoginFailure() + EventBus.publish("LOGIN_FAIL") + } + } + ) + } + + fun tokenCheckRequest() { + Log.d("Development", "Token check request to backend service") + val token = secretPreference.getToken() ?: "" + + Client.connect.tokenCheck("Bearer $token").enqueue( + object : Callback<UserCred> { + override fun onResponse(call: Call<UserCred>, response: Response<UserCred>) { + Log.d("Development", "Response: ${response.body()}") + if (response.isSuccessful) { + Log.d("Development", "Token check success: Valid token") + } + else if (response.code() == 401) { + Log.d("Development", "Token check failed: Invalid token or expired") + EventBus.publish("TOKEN_EXPIRED") + } + else { + Log.d("Development", "TOKEN CHECK FAILED, http code : ${response.code()}") + EventBus.publish("TOKEN_EXPIRED") + } + } + + override fun onFailure(call: Call<UserCred>, t: Throwable) { + Log.d("Development", "TOKEN CHECK FAILED, error on delivery : ${t.message}") } } ) } + + fun uploadBillRequest(file: File) { + Log.d("Development", "Upload bill request to backend service") + + val token : String = secretPreference.getToken() ?: "" + val requestFile = RequestBody.create(MediaType.parse("image/jpeg"), file) + val body = MultipartBody.Part.createFormData("file", file.name, requestFile) + + Client.connect.uploadBill("Bearer $token", body).enqueue( + object : Callback<BillResponse> { + override fun onResponse(call: Call<BillResponse>, response: Response<BillResponse>) { + Log.d("Development", "Response: $response") + if (response.isSuccessful) { + Log.i("Development", "Upload Success, response: ${response.body()}") + EventBus.publish("UPLOAD_SUCCESS") + } else { + Log.d("Development", "UPLOAD FAILED, http code : ${response.code()}") + EventBus.publish("UPLOAD_FAIL") + } + } + + override fun onFailure(call: Call<BillResponse>, t: Throwable) { + Log.d("Development", "UPLOAD FAILED, error on delivery : ${t.message}") + EventBus.publish("UPLOAD_FAIL") + } + } + ) + } + + } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt b/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt new file mode 100644 index 0000000000000000000000000000000000000000..3b6548d6386626d9c9cafa7584f2577676b8ea2a --- /dev/null +++ b/app/src/main/java/com/example/myapplication/service/TokenExpiryWorker.kt @@ -0,0 +1,23 @@ +package com.example.myapplication.service + +import android.content.Context +import android.util.Log +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import com.example.myapplication.repository.AuthRepository +import com.example.myapplication.util.LoginListener +import com.example.myapplication.util.SecretPreference + +class TokenExpiryWorker (appContext: Context, workerParams: WorkerParameters) + : CoroutineWorker(appContext, workerParams) { + + private val secretPreference = SecretPreference(applicationContext) + private val authRepository = AuthRepository(secretPreference) + + override suspend fun doWork(): Result { + Log.i("Development", "Token expiry worker started") + authRepository.tokenCheckRequest() + return Result.success() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/ui/dashboard/DashboardFragment.kt b/app/src/main/java/com/example/myapplication/ui/dashboard/DashboardFragment.kt index e417b48167f5c10e3e10ab5aacd415be5730a1fd..8258269c19efd914decbcc2cf49a0aee3d703cce 100644 --- a/app/src/main/java/com/example/myapplication/ui/dashboard/DashboardFragment.kt +++ b/app/src/main/java/com/example/myapplication/ui/dashboard/DashboardFragment.kt @@ -1,22 +1,102 @@ package com.example.myapplication.ui.dashboard +import android.Manifest +import android.annotation.SuppressLint +import android.content.ActivityNotFoundException +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.ImageFormat +import android.graphics.SurfaceTexture +import android.hardware.camera2.CameraCaptureSession +import android.hardware.camera2.CameraDevice +import android.hardware.camera2.CameraManager +import android.hardware.camera2.CaptureRequest +import android.hardware.camera2.params.OutputConfiguration +import android.hardware.camera2.params.SessionConfiguration +import android.media.ImageReader +import android.net.Uri import android.os.Bundle +import android.provider.MediaStore +import android.util.Log import android.view.LayoutInflater +import android.view.Surface +import android.view.TextureView import android.view.View import android.view.ViewGroup -import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider -import com.example.myapplication.databinding.FragmentDashboardBinding +import com.example.myapplication.databinding.FragmentScanBinding +import com.example.myapplication.repository.AuthRepository +import com.example.myapplication.util.SecretPreference +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.util.concurrent.Executor class DashboardFragment : Fragment() { - private var _binding: FragmentDashboardBinding? = null + private var _binding: FragmentScanBinding? = null // This property is only valid between onCreateView and // onDestroyView. private val binding get() = _binding!! + private lateinit var cameraId: String + private lateinit var cameraDevice: CameraDevice + private lateinit var captureRequestBuilder : CaptureRequest.Builder + private lateinit var mainExecutor : Executor + private var cameraPermission: Boolean = false + private lateinit var fileImage: File + private lateinit var imagePath : String + + private lateinit var authRepository : AuthRepository + + private val stateCallBack = object : CameraDevice.StateCallback() { + override fun onOpened(camera: CameraDevice) { + cameraDevice = camera + startCamera() + } + + override fun onDisconnected(camera: CameraDevice) { + cameraDevice.close() + } + + override fun onError(camera: CameraDevice, error: Int) { + cameraDevice.close() + } + } + + private val surfaceTextureListener = object : TextureView.SurfaceTextureListener { + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + Log.i("Development", "Surface texture available") + startCamera() + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + // Ignored, Camera does all the work for us + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + return false + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + // Ignored + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + authRepository = AuthRepository(SecretPreference(requireContext())) + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -25,18 +105,216 @@ class DashboardFragment : Fragment() { val dashboardViewModel = ViewModelProvider(this).get(DashboardViewModel::class.java) - _binding = FragmentDashboardBinding.inflate(inflater, container, false) - val root: View = binding.root + _binding = FragmentScanBinding.inflate(inflater, container, false) - val textView: TextView = binding.textDashboard - dashboardViewModel.text.observe(viewLifecycleOwner) { - textView.text = it + cameraPermissionCheck() + + binding.buttonScan.setOnClickListener { + takePicture() + } + + binding.buttonSend.setOnClickListener { + if(::imagePath.isInitialized) { + CoroutineScope(Dispatchers.IO).launch { + val uri = Uri.parse(imagePath) + val parcelFileDescriptor = requireContext().contentResolver.openFileDescriptor(uri, "r") + val file = File.createTempFile("upload", null, requireContext().cacheDir) + val inputStream = FileInputStream(parcelFileDescriptor?.fileDescriptor) + val outputStream = FileOutputStream(file) + inputStream.copyTo(outputStream) + + authRepository.uploadBillRequest(file) + + parcelFileDescriptor?.close() + } + } else { + Log.w("Development", "Image path not initialized") + } + } + + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + fileImage = File(requireContext().externalMediaDirs.first(), "\"${System.currentTimeMillis()}.jpg\"" ) + } + + override fun onResume() { + super.onResume() + if(cameraPermission) { + setupCamera() + binding.cameraPreview.surfaceTextureListener = surfaceTextureListener } - return root } override fun onDestroyView() { super.onDestroyView() _binding = null } + + // CAMERA + private fun cameraPermissionCheck() { + Log.i("Development", "Checking camera permission") + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_DENIED) { + registerForActivityResult( + ActivityResultContracts.RequestPermission() + ) { isGranted: Boolean -> + if (isGranted) { + // Permission is granted + Log.i("Development", "Camera permission granted") + cameraPermission = true + } else { + // Permission is denied + Log.w("Development", "Camera permission DENIED") + } + }.launch(Manifest.permission.CAMERA) + + } else { + cameraPermission = true + } + + if(cameraPermission) { + Log.i("Development", "Camera permission granted") + } + } + + private fun setupCamera() { + Log.i("Development", "Setting up camera") + if(ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + val cameraManager = + requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager + cameraId = cameraManager.cameraIdList[0] + Log.i("Development", "Camera ID: $cameraId") + cameraManager.openCamera(cameraId, stateCallBack, null) + Log.i("Development", "Camera opened") + } + } + @SuppressLint("Recycle") + private fun startCamera() { + val texture = binding.cameraPreview.surfaceTexture + if (texture != null) { + texture.setDefaultBufferSize(290, 420) + } else { + Log.e("Development", "Texture is null") + } + Log.i("Development", "Camera starting") + + val surface = Surface(texture) + captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) + captureRequestBuilder.addTarget(surface) + + cameraDevice.createCaptureSession(listOf(surface), object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + session.setRepeatingRequest(captureRequestBuilder.build(), null, null) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + Log.w("Development", "Camera configuration failed") + } + }, null) + +// val outputConfig : OutputConfiguration = OutputConfiguration(surface) +// mainExecutor = ContextCompat.getMainExecutor(requireContext()) +// +// val sessionConfig : SessionConfiguration = SessionConfiguration( +// SessionConfiguration.SESSION_REGULAR, +// listOf(outputConfig), +// mainExecutor, +// cameraCaptureStateCallback) +// +// cameraDevice.createCaptureSession(sessionConfig) + + } + + val REQUEST_IMAGE_CAPTURE = 1 + + private fun dispatchTakePictureIntent() { + val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + try { + startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE) + } catch (e: ActivityNotFoundException) { + // display error state to the user + } + } + + + private fun takePicture() { + val captureRequestBuilderTake = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE) + val imageReader = ImageReader.newInstance(290, 420, ImageFormat.JPEG, 1) + + imageReader.setOnImageAvailableListener({ reader -> + Log.i("Development", "image listener called") + reader.acquireNextImage().use { image -> + val buffer = image.planes[0].buffer + val bytes = ByteArray(buffer.remaining()) + buffer.get(bytes) + saveImage(bytes) + Log.i("Development", "Image saved") + } + }, null) + + + captureRequestBuilderTake.addTarget(imageReader.surface) + captureRequestBuilderTake.addTarget(Surface(binding.cameraPreview.surfaceTexture)) + + val outputConfig = OutputConfiguration(Surface(binding.cameraPreview.surfaceTexture)) + val outputConfigReader = OutputConfiguration(imageReader.surface) + + val sessionConfig = SessionConfiguration(SessionConfiguration.SESSION_REGULAR, + listOf(outputConfig, outputConfigReader), ContextCompat.getMainExecutor(requireContext()), object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + session.capture(captureRequestBuilderTake.build(), null, null) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + Log.e("Development", "Capture session configuration failed") + } + }) + + cameraDevice.createCaptureSession(sessionConfig) + } + + private fun saveImage(file: ByteArray) { + val resolver = requireContext().contentResolver + val contentValues = ContentValues().apply { + put(MediaStore.MediaColumns.DISPLAY_NAME, "Image_${System.currentTimeMillis()}.jpg") + put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + put(MediaStore.MediaColumns.IS_PENDING, 1) + } + } + + val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) + + uri?.let { + resolver.openOutputStream(it)?.use { outputStream -> + outputStream.write(file) + } + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + contentValues.clear() + contentValues.put(MediaStore.MediaColumns.IS_PENDING, 0) + resolver.update(uri, contentValues, null, null) + + } + imagePath = it.toString() + Log.i("Development", "Image saved to $it") + } + + } + + + val cameraCaptureStateCallback = object : CameraCaptureSession.StateCallback() { + override fun onConfigured(session: CameraCaptureSession) { + session.setRepeatingRequest(captureRequestBuilder.build(), null, null) + } + + override fun onConfigureFailed(session: CameraCaptureSession) { + Log.w("Development", "Camera configuration failed") + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/util/EventBus.kt b/app/src/main/java/com/example/myapplication/util/EventBus.kt new file mode 100644 index 0000000000000000000000000000000000000000..a3b8bb06f325991835b7b0fd00dfa89befe902b3 --- /dev/null +++ b/app/src/main/java/com/example/myapplication/util/EventBus.kt @@ -0,0 +1,28 @@ +package com.example.myapplication.util + +import android.util.Log + +object EventBus { + + private var listeners: MutableMap<String, MutableList<() -> Unit>> = mutableMapOf() + + fun subscribe(event: String, listener: () -> Unit) { + if (listeners.containsKey(event)) { + // add a new listener to an existing event + listeners[event]?.add(listener) + } else { + // add a new event with a new listener + listeners[event] = mutableListOf(listener) + } + } + + fun publish(event: String) { + if (listeners.containsKey(event)) { + // call all listeners for the event + listeners[event]?.forEach { it() } + } else { + // no listeners for the event + Log.d("Development","No listeners for event: $event") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/myapplication/util/SecretPreference.kt b/app/src/main/java/com/example/myapplication/util/SecretPreference.kt index e0fd205f8288888970b764b9cbc80732e2388b7d..8569b03b5d2391629dafa9ff5a274ca3784bc677 100644 --- a/app/src/main/java/com/example/myapplication/util/SecretPreference.kt +++ b/app/src/main/java/com/example/myapplication/util/SecretPreference.kt @@ -25,4 +25,9 @@ class SecretPreference (private val context: Context) { fun getToken(): String? = sharedPreferences.getString("token", null) + fun clearToken(){ + sharedPreferences.edit().remove("token").apply() + Log.i("Development", "Token cleared") + } + } \ 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..5901b3252192ff780eabb16591041da6ddc2f80b --- /dev/null +++ b/app/src/main/res/layout/fragment_scan.xml @@ -0,0 +1,40 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <TextureView + android:id="@+id/camera_preview" + android:layout_width="290dp" + android:layout_height="420dp" + 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.311" /> + + <Button + android:id="@+id/button_scan" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:text="Capture" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.315" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/camera_preview" /> + + <Button + android:id="@+id/button_send" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="Send" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0.254" + app:layout_constraintStart_toEndOf="@id/button_scan" + app:layout_constraintTop_toBottomOf="@id/camera_preview" + app:layout_constraintVertical_bias="0.12" /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2a937b35557de4690764cd6b2547db0d9aafc2d0..8d5a450e74d343773a514fda3cb1a1bbbdc7efc1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ lifecycleLivedataKtx = "2.7.0" lifecycleViewmodelKtx = "2.7.0" navigationFragmentKtx = "2.6.0" navigationUiKtx = "2.6.0" +workRuntimeKtx = "2.9.0" [libraries] android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawable" } @@ -27,6 +28,7 @@ androidx-lifecycle-livedata-ktx = { group = "androidx.lifecycle", name = "lifecy androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" } androidx-navigation-fragment-ktx = { group = "androidx.navigation", name = "navigation-fragment-ktx", version.ref = "navigationFragmentKtx" } androidx-navigation-ui-ktx = { group = "androidx.navigation", name = "navigation-ui-ktx", version.ref = "navigationUiKtx" } +androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }