diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbf15dec27d23379f24fbf910881a32c5b8bc9c9..c5b147c31e0d5c4a7aea53b41936dbccd4f5dcf8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -71,5 +71,5 @@ dependencies { androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") - implementation("org.jsoup:jsoup:1.14.3") + implementation("androidx.datastore:datastore-preferences:1.0.0") } \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/MainActivity.kt b/app/src/main/java/com/example/abe/MainActivity.kt index 0cf3ec2fedbeb06fd9ad4b595ff36dcc9efe98de..1c2c332d9497191ed587f3585c6f5a2da6ce1b49 100644 --- a/app/src/main/java/com/example/abe/MainActivity.kt +++ b/app/src/main/java/com/example/abe/MainActivity.kt @@ -23,6 +23,9 @@ import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController import com.example.abe.connection.ConnectivityObserver import com.example.abe.connection.NetworkConnectivityObserver +import com.example.abe.data.local.PreferenceDataStoreConstants +import com.example.abe.data.local.PreferenceDataStoreConstants.USER +import com.example.abe.data.local.PreferenceDataStoreHelper import com.example.abe.databinding.ActivityMainBinding import com.example.abe.services.AuthService import com.example.abe.types.FragmentListener @@ -73,6 +76,10 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD } "EXPIRED_TOKEN" -> { + lifecycleScope.launch { + preferenceDataStoreHelper.putPreference(PreferenceDataStoreConstants.TOKEN,"") + preferenceDataStoreHelper.putPreference(PreferenceDataStoreConstants.USER,"") + } val loginIntent = Intent(context, LoginActivity::class.java) startActivity(loginIntent) this@MainActivity.finish() @@ -81,9 +88,13 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD } } + lateinit var preferenceDataStoreHelper: PreferenceDataStoreHelper + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + preferenceDataStoreHelper = PreferenceDataStoreHelper(applicationContext) + binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) @@ -109,9 +120,6 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD LocalBroadcastManager.getInstance(this).registerReceiver(br, filter) - val serviceIntent = Intent(this, AuthService::class.java) - startService(serviceIntent) - connectivityObserver = NetworkConnectivityObserver(applicationContext) connectivityObserver.observe().onEach { networkState = it @@ -129,11 +137,13 @@ class MainActivity : AppCompatActivity(), ExportAlertDialogFragment.ExportAlertD } }.launchIn(lifecycleScope) - val sharedPref = getSharedPreferences( - getString(R.string.preference_file_key), - Context.MODE_PRIVATE - ) - user = sharedPref.getString("user", "").toString() + + lifecycleScope.launch { + user = preferenceDataStoreHelper.getFirstPreference(USER,"") + } + val serviceIntent = Intent(this@MainActivity, AuthService::class.java) + startService(serviceIntent) + } fun getNetworkState(): ConnectivityObserver.NetworkState { diff --git a/app/src/main/java/com/example/abe/data/local/IPreferenceDataStore.kt b/app/src/main/java/com/example/abe/data/local/IPreferenceDataStore.kt new file mode 100644 index 0000000000000000000000000000000000000000..fdaef4708cb2f58cc6badbb11dcfcfd08ce42486 --- /dev/null +++ b/app/src/main/java/com/example/abe/data/local/IPreferenceDataStore.kt @@ -0,0 +1,12 @@ +package com.example.abe.data.local + +import androidx.datastore.preferences.core.Preferences +import kotlinx.coroutines.flow.Flow + +interface IPreferenceDataStore { + suspend fun <T> getPreference(key: Preferences.Key<T>,defaultValue: T):Flow<T> + suspend fun <T> getFirstPreference(key: Preferences.Key<T>,defaultValue: T):T + suspend fun <T> putPreference(key: Preferences.Key<T>,value:T) + suspend fun <T> removePreference(key: Preferences.Key<T>) + suspend fun <T> clearAllPreference() +} \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreConstants.kt b/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreConstants.kt new file mode 100644 index 0000000000000000000000000000000000000000..576322b342de156997934d4fececc78ee20f2fb7 --- /dev/null +++ b/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreConstants.kt @@ -0,0 +1,8 @@ +package com.example.abe.data.local + +import androidx.datastore.preferences.core.stringPreferencesKey + +object PreferenceDataStoreConstants { + val TOKEN = stringPreferencesKey("TOKEN") + val USER = stringPreferencesKey("USER") +} \ No newline at end of file diff --git a/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreHelper.kt b/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreHelper.kt new file mode 100644 index 0000000000000000000000000000000000000000..45416a3edb0785328a1d6e8a811abdecb582a00e --- /dev/null +++ b/app/src/main/java/com/example/abe/data/local/PreferenceDataStoreHelper.kt @@ -0,0 +1,53 @@ +package com.example.abe.data.local + +import android.content.Context +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.emptyPreferences +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import java.io.IOException + +private val Context.dataStore by preferencesDataStore( + name = "PreferenceDataStore" +) +class PreferenceDataStoreHelper(context: Context):IPreferenceDataStore { + + private val dataSource = context.dataStore + + override suspend fun <T> getPreference(key: Preferences.Key<T>, defaultValue: T): + Flow<T> = dataSource.data.catch { exception -> + if (exception is IOException){ + emit(emptyPreferences()) + }else{ + throw exception + } + }.map { preferences-> + val result = preferences[key]?: defaultValue + result + } + + override suspend fun <T> getFirstPreference(key: Preferences.Key<T>, defaultValue: T) : + T = dataSource.data.first()[key] ?: defaultValue + + override suspend fun <T> putPreference(key: Preferences.Key<T>, value: T) { + dataSource.edit { preferences -> + preferences[key] = value + } + } + + override suspend fun <T> removePreference(key: Preferences.Key<T>) { + dataSource.edit { preferences -> + preferences.remove(key) + } + } + + override suspend fun <T> clearAllPreference() { + dataSource.edit { preferences -> + preferences.clear() + } + } +} \ No newline at end of file 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 fe589397ae144b2cd8e8805ab8053fc31301d6aa..bd9b8605b78f7b6f4714a7d04256661d3cb9bc53 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,9 +1,6 @@ package com.example.abe.data.network -import android.content.Context import android.util.Log -import com.example.abe.R -import com.google.gson.Gson import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.OkHttpClient @@ -90,10 +87,9 @@ class Retrofit { }) } - fun upload(context: Context, file: File, callback: UploadResultCallback) { + fun upload(token: String, 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", "") + val authHeader = "Bearer $token" // Determine the MIME type of the file val mimeType = URLConnection.guessContentTypeFromName(file.name) diff --git a/app/src/main/java/com/example/abe/services/AuthService.kt b/app/src/main/java/com/example/abe/services/AuthService.kt index 131e986a73fc3a1690fd9ff8573b84af43215a7a..6ebef0d8dd060898c451110a0243b2fbcbe79dde 100644 --- a/app/src/main/java/com/example/abe/services/AuthService.kt +++ b/app/src/main/java/com/example/abe/services/AuthService.kt @@ -1,45 +1,45 @@ package com.example.abe.services -import android.app.Service -import android.content.Context import android.content.Intent -import android.os.Handler import android.os.IBinder -import android.os.Looper -import android.util.Log -import androidx.core.content.ContentProviderCompat.requireContext +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.example.abe.R -import com.example.abe.data.network.CheckAuthResponse +import com.example.abe.data.local.PreferenceDataStoreConstants +import com.example.abe.data.local.PreferenceDataStoreHelper import com.example.abe.data.network.CheckAuthResultCallback import com.example.abe.data.network.Retrofit +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch -class AuthService : Service(), CheckAuthResultCallback { +class AuthService : LifecycleService(), CheckAuthResultCallback { var isRunning: Boolean = false + private lateinit var preferenceDataStoreHelper: PreferenceDataStoreHelper + override fun onCreate() { + super.onCreate() + preferenceDataStoreHelper = PreferenceDataStoreHelper(applicationContext) + } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) isRunning = true - Thread { + lifecycleScope.launch { while (isRunning) { val retrofit = Retrofit() - val sharedPref = getSharedPreferences( - getString(R.string.preference_file_key), - Context.MODE_PRIVATE + val token = preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.TOKEN, + "" ) - val token = sharedPref.getString("login_token", "").toString() - retrofit.checkAuth(token, this) - try { - Thread.sleep(30000) - } catch (e: InterruptedException) { - e.printStackTrace() - } + retrofit.checkAuth(token, this@AuthService) + delay(30000) } - }.start() + } return START_NOT_STICKY } - override fun onBind(intent: Intent?): IBinder? { + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) return null } @@ -51,6 +51,7 @@ class AuthService : Service(), CheckAuthResultCallback { } override fun onDestroy() { + super.onDestroy() isRunning = false } } \ 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 386b625c8015d68f32910c51e5614c7e847810b5..c7e3ae5f92093e3a48a318dee8cd78b0fbe52f8a 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 @@ -12,7 +12,6 @@ import android.location.Location import android.location.LocationManager import android.net.Uri import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -26,14 +25,18 @@ import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.ActivityCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.example.abe.ABEApplication +import com.example.abe.MainActivity import com.example.abe.R +import com.example.abe.data.local.PreferenceDataStoreConstants import com.example.abe.databinding.FragmentFormTransactionBinding import com.example.abe.utils.isNumericValid import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices import com.google.android.material.textfield.TextInputLayout +import kotlinx.coroutines.launch import java.util.Locale class FormTransaction : Fragment() { @@ -94,10 +97,10 @@ class FormTransaction : Fragment() { getLocation() - val sharedPref = requireActivity().getSharedPreferences( - getString(R.string.preference_file_key), Context.MODE_PRIVATE - ) - user = sharedPref.getString("user", "").toString() + lifecycleScope.launch { + user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.USER,"") + } if (arguments != null) { val args = Bundle(arguments) diff --git a/app/src/main/java/com/example/abe/ui/graph/GraphFragment.kt b/app/src/main/java/com/example/abe/ui/graph/GraphFragment.kt index d359ab0a31def7f395ccccc5553d50fa6073bdbf..704e8e5ad862176cf069e4fd194db89b723cad24 100644 --- a/app/src/main/java/com/example/abe/ui/graph/GraphFragment.kt +++ b/app/src/main/java/com/example/abe/ui/graph/GraphFragment.kt @@ -1,6 +1,5 @@ package com.example.abe.ui.graph -import android.content.Context import android.graphics.Color import android.os.Bundle import android.view.LayoutInflater @@ -10,7 +9,9 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.example.abe.ABEApplication +import com.example.abe.MainActivity import com.example.abe.R +import com.example.abe.data.local.PreferenceDataStoreConstants import com.example.abe.databinding.FragmentGraphBinding import com.example.abe.domain.FormatCurrencyUseCase import com.github.mikephil.charting.data.PieData @@ -36,16 +37,16 @@ class GraphFragment : Fragment() { ): View { _binding = FragmentGraphBinding.inflate(inflater, container, false) - drawGraph() + lifecycleScope.launch { + val user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.USER,"") + drawGraph(user) + } + return binding.root } - private fun drawGraph() = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { - val sharedPref = requireActivity().getSharedPreferences( - getString(R.string.preference_file_key), - Context.MODE_PRIVATE - ) - val user = sharedPref.getString("user", "").toString() + private fun drawGraph(user: String) = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { val expenses = viewModel.getExpenses(user).toDouble() val income = viewModel.getIncome(user).toDouble() 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 157d02343ae6efd2c70b84c5acf2136a67c638e3..b562f9192cfc49650f088827b0d7610756297e22 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,5 @@ package com.example.abe.ui.login -import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Patterns @@ -11,9 +10,11 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.example.abe.MainActivity -import com.example.abe.R import com.example.abe.connection.ConnectivityObserver import com.example.abe.connection.NetworkConnectivityObserver +import com.example.abe.data.local.PreferenceDataStoreConstants.TOKEN +import com.example.abe.data.local.PreferenceDataStoreConstants.USER +import com.example.abe.data.local.PreferenceDataStoreHelper import com.example.abe.data.network.LoginResultCallback import com.example.abe.data.network.Retrofit import com.example.abe.databinding.ActivityLoginBinding @@ -21,6 +22,7 @@ import com.example.abe.utils.isConnected import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch class LoginActivity : AppCompatActivity(), LoginResultCallback { @@ -40,20 +42,19 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { private lateinit var connectivityObserver: ConnectivityObserver private var networkState: ConnectivityObserver.NetworkState? = null + private lateinit var preferenceDataStoreHelper: PreferenceDataStoreHelper + override fun onSuccess(loginResponse: com.example.abe.data.network.LoginResponse) { println("Login successful: $loginResponse") - val sharedPref = - getSharedPreferences(getString(R.string.preference_file_key), Context.MODE_PRIVATE) - with(sharedPref.edit()) { - putString("login_token", loginResponse.token) - putString("user", email) - apply() + lifecycleScope.launch { + preferenceDataStoreHelper.putPreference(TOKEN, loginResponse.token) + preferenceDataStoreHelper.putPreference(USER, email) + val intent = Intent(this@LoginActivity, MainActivity::class.java) + startActivity(intent) + finish() } - val intent = Intent(this, MainActivity::class.java) - startActivity(intent) - finish() } override fun onFailure(errorMessage: String) { @@ -86,6 +87,8 @@ class LoginActivity : AppCompatActivity(), LoginResultCallback { } }.launchIn(lifecycleScope) + preferenceDataStoreHelper = PreferenceDataStoreHelper(applicationContext) + binding.btnSignIn.setOnClickListener { setHelperText(binding.formEmailContainer, binding.emailInput, true) setHelperText(binding.formPasswordContainer, binding.passwordInput, false) 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 23b6ec0c177ba30957dd505a629e8f35524634ac..c3a4f37f34228a5bfeee7ee3a018c6f78a7bacdb 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 @@ -33,11 +33,13 @@ import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.bumptech.glide.Glide import com.example.abe.ABEApplication import com.example.abe.MainActivity import com.example.abe.R +import com.example.abe.data.local.PreferenceDataStoreConstants import com.example.abe.data.network.ItemsRoot import com.example.abe.data.network.Retrofit import com.example.abe.data.network.UploadResultCallback @@ -45,6 +47,7 @@ import com.example.abe.databinding.FragmentScanBinding import com.example.abe.utils.isConnected import com.google.android.gms.location.FusedLocationProviderClient import com.google.android.gms.location.LocationServices +import kotlinx.coroutines.launch import java.io.File import java.text.SimpleDateFormat import java.util.Locale @@ -133,11 +136,10 @@ class ScannerFragment : Fragment(), UploadResultCallback { startCamera() } - val sharedPref = activity?.getSharedPreferences( - getString(R.string.preference_file_key), - Context.MODE_PRIVATE - ) - user = sharedPref?.getString("user", "").toString() + lifecycleScope.launch { + user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference( + PreferenceDataStoreConstants.USER,"") + } binding.captureButton.setOnClickListener { takePicture() @@ -186,9 +188,11 @@ class ScannerFragment : Fragment(), UploadResultCallback { } private fun attemptUpload(imageFile: File) { - val retrofit = Retrofit() - val context = requireContext() - retrofit.upload(context, imageFile, this) + lifecycleScope.launch { + val retrofit = Retrofit() + val token = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference(PreferenceDataStoreConstants.TOKEN, "") + retrofit.upload(token, imageFile, this@ScannerFragment) + } } private fun showPreviewDialog(imageUri: Uri) { diff --git a/app/src/main/java/com/example/abe/ui/transactions/TransactionFragment.kt b/app/src/main/java/com/example/abe/ui/transactions/TransactionFragment.kt index a9604587b5fc95659f41aae0fff8b89acb75c140..695b7b48bf07648d6064dc1ba6bb224d05f4d5b1 100644 --- a/app/src/main/java/com/example/abe/ui/transactions/TransactionFragment.kt +++ b/app/src/main/java/com/example/abe/ui/transactions/TransactionFragment.kt @@ -7,11 +7,14 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.example.abe.ABEApplication -import com.example.abe.R +import com.example.abe.MainActivity +import com.example.abe.data.local.PreferenceDataStoreConstants import com.example.abe.databinding.FragmentTransactionsBinding import com.example.abe.types.FragmentListener +import kotlinx.coroutines.launch class TransactionFragment : Fragment() { @@ -41,16 +44,12 @@ class TransactionFragment : Fragment() { binding.rvTransactions.adapter = transactionsAdapter binding.rvTransactions.layoutManager = LinearLayoutManager(context) - val sharedPref = requireActivity().getSharedPreferences( - getString(R.string.preference_file_key), - Context.MODE_PRIVATE - ) - val user = sharedPref.getString("user", "").toString() - -// TODO: check only for transactions by current user - viewModel.getAllTransactions(user).observe(viewLifecycleOwner) { transactions -> - transactions?.let { - transactionsAdapter.submitList(it) + lifecycleScope.launch { + val user = (activity as MainActivity).preferenceDataStoreHelper.getFirstPreference(PreferenceDataStoreConstants.USER,"") + viewModel.getAllTransactions(user).observe(viewLifecycleOwner) { transactions -> + transactions?.let { + transactionsAdapter.submitList(it) + } } } diff --git a/app/src/main/java/com/example/abe/utils/Utils.kt b/app/src/main/java/com/example/abe/utils/Utils.kt index d3ff7f6ebeb93595dbe210d65b39eef651f4e1f2..40c9521134e935523054d09090f8abe31c2b6e46 100644 --- a/app/src/main/java/com/example/abe/utils/Utils.kt +++ b/app/src/main/java/com/example/abe/utils/Utils.kt @@ -1,8 +1,6 @@ package com.example.abe.utils import com.example.abe.connection.ConnectivityObserver -import org.jsoup.Jsoup -import org.jsoup.safety.Safelist fun isConnected(networkState: ConnectivityObserver.NetworkState?): Boolean { return (networkState != null && (networkState == ConnectivityObserver.NetworkState.AVAILABLE || networkState == ConnectivityObserver.NetworkState.LOSING)) @@ -11,7 +9,3 @@ fun isConnected(networkState: ConnectivityObserver.NetworkState?): Boolean { fun isNumericValid(input: String): Boolean { return input.matches("\\d+".toRegex()) } - -fun sanitizeHtml(input: String): String { - return Jsoup.clean(input, Safelist.basic()) -}