diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c4ad23e6cda9487d7865e4328bdfd3c10d7a86d9..ba17d99cdc9f0e00611e02631089e70ea24e131f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,12 +7,15 @@ <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.CAMERA" /> + + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> - <uses-permission - android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.INTERNET" /> <application android:name=".MainApplication" @@ -35,6 +38,7 @@ <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> + <service android:name=".services.services.ExpiryService" /> </application> diff --git a/app/src/main/java/com/example/bondoman/data/dataaccess/TransactionDao.kt b/app/src/main/java/com/example/bondoman/data/dataaccess/TransactionDao.kt index f795d8a85fba16282f9074831a4de965d97f9a6f..73387b7239692c47821fdaf0f15e22e922c92cc6 100644 --- a/app/src/main/java/com/example/bondoman/data/dataaccess/TransactionDao.kt +++ b/app/src/main/java/com/example/bondoman/data/dataaccess/TransactionDao.kt @@ -3,7 +3,9 @@ package com.example.bondoman.data.dataaccess import androidx.room.Dao import androidx.room.Insert import androidx.room.Query +import com.example.bondoman.data.models.CategoryTotal import com.example.bondoman.data.models.Transaction +import java.util.Date @Dao interface TransactionDao { @@ -33,4 +35,15 @@ interface TransactionDao { id: Long, owner: String, ) + + @Query( + "SELECT category, TOTAL(amount) AS totalAmount " + + "FROM transactions " + + "WHERE owner = :owner AND date >= :beginDate AND date < :endDate GROUP BY category", + ) + suspend fun getCategoryTotals( + owner: String, + beginDate: Date, + endDate: Date, + ): List<CategoryTotal> } diff --git a/app/src/main/java/com/example/bondoman/data/models/Transaction.kt b/app/src/main/java/com/example/bondoman/data/models/Transaction.kt index dc9b44256a578bd5afc621d106e5bb03b9f52530..8461d7817e707d41fae530ebf117ebd01204da39 100644 --- a/app/src/main/java/com/example/bondoman/data/models/Transaction.kt +++ b/app/src/main/java/com/example/bondoman/data/models/Transaction.kt @@ -10,6 +10,11 @@ enum class TransactionCategory(val string: String) { EXPENSE("Outcome"), } +data class CategoryTotal( + val category: TransactionCategory, + val totalAmount: Double, +) + val transactionCategoryMap: Map<String, TransactionCategory> = mapOf( "Income" to TransactionCategory.EARNINGS, diff --git a/app/src/main/java/com/example/bondoman/data/repositories/TransactionRepository.kt b/app/src/main/java/com/example/bondoman/data/repositories/TransactionRepository.kt index a840fb70da0d869a3debe4257418c4b849e3b691..d57eeee6701eb04e377d543976d6e03288819f70 100644 --- a/app/src/main/java/com/example/bondoman/data/repositories/TransactionRepository.kt +++ b/app/src/main/java/com/example/bondoman/data/repositories/TransactionRepository.kt @@ -2,6 +2,7 @@ package com.example.bondoman.data.repositories import com.example.bondoman.data.dataaccess.TransactionDao import com.example.bondoman.data.models.Transaction +import java.util.Date class TransactionRepository(private val transactionDao: TransactionDao) { suspend fun getAll(owner: String) = transactionDao.getAll(owner) @@ -25,4 +26,10 @@ class TransactionRepository(private val transactionDao: TransactionDao) { id: Long, owner: String, ) = transactionDao.delete(id, owner) + + suspend fun getCategoryTotals( + owner: String, + beginDate: Date, + endDate: Date, + ) = transactionDao.getCategoryTotals(owner, beginDate, endDate) } diff --git a/app/src/main/java/com/example/bondoman/data/utils/TransactionParser.kt b/app/src/main/java/com/example/bondoman/data/utils/TransactionParser.kt index ba2c947167eb83ce7855b1d7c6df540b2495f2f1..72b7e9c8d4f6ae763c071d074d1112d00f4ba5c4 100644 --- a/app/src/main/java/com/example/bondoman/data/utils/TransactionParser.kt +++ b/app/src/main/java/com/example/bondoman/data/utils/TransactionParser.kt @@ -1,6 +1,8 @@ package com.example.bondoman.data.utils +import android.util.Log import com.example.bondoman.data.models.TransactionCategory +import java.text.DecimalFormat import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -28,10 +30,6 @@ class TransactionParser { return formattedBuilder.reverse().toString() } - fun getDateString(date: Date): String { - return dateFormat.format(date) - } - fun getAmountString( amount: Long, category: TransactionCategory, @@ -40,4 +38,23 @@ class TransactionParser { val amountFormatted = formatAmountString(amount.toString()) return "$sign Rp$amountFormatted" } + + fun getAmountString( + amount: Double, + category: TransactionCategory, + ): String { + val sign = if (category == TransactionCategory.EXPENSE) "-" else "" + + val decimalFormat = DecimalFormat("0.00") + val formattedAmount = decimalFormat.format(amount) + + val parts = formattedAmount.split(".") + val integerPart = parts[0] + val amountFormatted = formatAmountString(integerPart) + return "$sign Rp$amountFormatted" + } + + fun getDateString(date: Date): String { + return dateFormat.format(date) + } } diff --git a/app/src/main/java/com/example/bondoman/data/viewmodels/transaction/TransactionViewModel.kt b/app/src/main/java/com/example/bondoman/data/viewmodels/transaction/TransactionViewModel.kt index f0fbe75ae82254b270bb69d508f557b98aad4c5d..2f61bb3a0f89aea1e4afebf6f6fea085cb3fc91e 100644 --- a/app/src/main/java/com/example/bondoman/data/viewmodels/transaction/TransactionViewModel.kt +++ b/app/src/main/java/com/example/bondoman/data/viewmodels/transaction/TransactionViewModel.kt @@ -5,10 +5,12 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.example.bondoman.data.models.CategoryTotal import com.example.bondoman.data.models.Transaction import com.example.bondoman.data.models.TransactionCategory import com.example.bondoman.data.repositories.TransactionRepository import kotlinx.coroutines.launch +import java.util.Date class TransactionViewModel( private val transactionRepository: TransactionRepository, @@ -18,9 +20,11 @@ class TransactionViewModel( private val currentUserNim = userNim private val _currentTransaction = MutableLiveData<Transaction?>() private val _transactions = MutableLiveData<List<Transaction>>() + private val _categoryTotals = MutableLiveData<List<CategoryTotal>>() val currentTransaction: LiveData<Transaction?> = _currentTransaction val transactions: LiveData<List<Transaction>> = _transactions + val categoryTotals: LiveData<List<CategoryTotal>> = _categoryTotals fun setCurrentTransaction(transaction: Transaction) { _currentTransaction.value = transaction @@ -52,7 +56,7 @@ class TransactionViewModel( Transaction( title = title, owner = currentUserNim, - amount = amount, + amount = Long.MAX_VALUE, category = category, location = location, ) @@ -120,4 +124,18 @@ class TransactionViewModel( } } } + + fun getTransactionTotals( + beginDate: Date, + endDate: Date, + ) { + viewModelScope.launch { + try { + _categoryTotals.value = + transactionRepository.getCategoryTotals(currentUserNim, beginDate, endDate) + } catch (e: Exception) { + Log.e("TransactionViewModel", e.toString()) + } + } + } } diff --git a/app/src/main/java/com/example/bondoman/views/activities/MainActivity.kt b/app/src/main/java/com/example/bondoman/views/activities/MainActivity.kt index bfa772e1bf07ce4a8b134fc50a8bd336235e7275..25cee9d2da3e3b80be271cebab49d38ca9fc6082 100644 --- a/app/src/main/java/com/example/bondoman/views/activities/MainActivity.kt +++ b/app/src/main/java/com/example/bondoman/views/activities/MainActivity.kt @@ -4,7 +4,13 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter +import android.net.ConnectivityManager +import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.View import android.view.ViewGroup import android.widget.ImageButton @@ -55,6 +61,8 @@ class MainActivity : AppCompatActivity(), ParentActivityService { private val expiryReceiver = ExpiryBroadcastReceiver() + private var isConnectionlost: Boolean = false + var randomizeNextTransaction: Boolean = false private val randomizeReceiver = object : BroadcastReceiver() { @@ -106,7 +114,10 @@ class MainActivity : AppCompatActivity(), ParentActivityService { override fun hideBackButton() { backButton.visibility = View.GONE - backButton.setOnClickListener { navController.popBackStack() } + + backButton.setOnClickListener { + navController.popBackStack() + } } override fun setHeaderText(text: String) { @@ -186,26 +197,54 @@ class MainActivity : AppCompatActivity(), ParentActivityService { navbar.setupWithNavController(navController) navController.addOnDestinationChangedListener { _, destination, _ -> - headerText.text = destination.label + headerText.text = destination.label ?: headerText.text destinationListeners.forEach { it.invoke(destination.id) } + // handle connection lost fragment + if (isConnectionlost) { + if (destination.id != R.id.ConnectionLostFragment) { + navController.navigate(R.id.action_global_to_ConnectionLostFragment) + } else { + currentFragmentId = R.id.ConnectionLostFragment + } + + return@addOnDestinationChangedListener + } + + // ignore destination if (currentFragmentId == destination.id) { return@addOnDestinationChangedListener - } else if (currentFragmentId != R.id.twibbonFragment && destination.id == R.id.twibbonPreviewFragment) { + } + + // handle twibbon preview navigation + else if ( + (currentFragmentId != R.id.twibbonFragment && currentFragmentId != R.id.ConnectionLostFragment) + && destination.id == R.id.twibbonPreviewFragment + ) { // Set the action to go back to TwibbonFragment navController.popBackStack(R.id.twibbonFragment, false) currentFragmentId = R.id.twibbonFragment - } else if (currentFragmentId != R.id.transactionListFragment && - (destination.id == R.id.transactionAddFragment || destination.id == R.id.transactionUpdateFragment) + } + + // handle navigation to add and update page + else if ( + (currentFragmentId != R.id.transactionListFragment && currentFragmentId != R.id.ConnectionLostFragment) + && (destination.id == R.id.transactionAddFragment || destination.id == R.id.transactionUpdateFragment) ) { // Set the action to go back to TransactionListFragment navController.popBackStack(R.id.transactionListFragment, false) currentFragmentId = R.id.transactionAddFragment - } else if (currentFragmentId != R.id.scanReceiptFragment && destination.id == R.id.scanReceiptResultFragment) { + } + + // handle scan fragment navigation + else if (currentFragmentId != R.id.scanReceiptFragment && destination.id == R.id.scanReceiptResultFragment) { navController.popBackStack(R.id.scanReceiptFragment, false) currentFragmentId = R.id.scanReceiptFragment - } else { + } + + // handle default behaviour + else { if (destination.id in MAIN_FRAGMENT_IDS) { hideBackButton() } @@ -222,6 +261,55 @@ class MainActivity : AppCompatActivity(), ParentActivityService { hideBackButton() } + private fun configureBroadcastReceiver() { + // IntentFilter + val expiryReceiverFilter = IntentFilter("com.example.bondoman.services.LOGOUT_TIMEOUT") + val randomizeReceiverFilter = + IntentFilter("com.example.bondoman.intents.RANDOMIZE_TRANSACTION") + + // Register Expiry Receiver + LocalBroadcastManager.getInstance(this) + .registerReceiver(expiryReceiver, expiryReceiverFilter) + LocalBroadcastManager.getInstance(this) + .registerReceiver(randomizeReceiver, randomizeReceiverFilter) + } + + private fun monitorConnection() { + val connectivityManager = + getSystemService(ConnectivityManager::class.java) as ConnectivityManager + connectivityManager.registerDefaultNetworkCallback(object : + ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + Handler(Looper.getMainLooper()).post { + if (isConnectionlost) { + isConnectionlost = false + navController.popBackStack() + } + } + } + + override fun onLost(network: Network) { + Handler(Looper.getMainLooper()).post { + isConnectionlost = true + navController.navigate(R.id.action_global_to_ConnectionLostFragment) + } + } + + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + } + + override fun onLinkPropertiesChanged( + network: Network, + linkProperties: LinkProperties + ) { + } + } + ) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -239,18 +327,13 @@ class MainActivity : AppCompatActivity(), ParentActivityService { // initialize fragment id tracking currentFragmentId = null - // IntentFilter - val expiryReceiverFilter = IntentFilter("com.example.bondoman.services.LOGOUT_TIMEOUT") - val randomizeReceiverFilter = IntentFilter("com.example.bondoman.intents.RANDOMIZE_TRANSACTION") - - // Register Expiry Receiver - LocalBroadcastManager.getInstance(this) - .registerReceiver(expiryReceiver, expiryReceiverFilter) - LocalBroadcastManager.getInstance(this) - .registerReceiver(randomizeReceiver, randomizeReceiverFilter) + configureBroadcastReceiver() initializeComponents() configureNavigation() + + // monitor network + monitorConnection() } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/java/com/example/bondoman/views/adapters/TransactionListAdapter.kt b/app/src/main/java/com/example/bondoman/views/adapters/TransactionListAdapter.kt index d8f96fea483e60aa687e4ce2650a5bc658b93239..1083d8932462f89a32f8a78cf7ec0a85560279e7 100644 --- a/app/src/main/java/com/example/bondoman/views/adapters/TransactionListAdapter.kt +++ b/app/src/main/java/com/example/bondoman/views/adapters/TransactionListAdapter.kt @@ -25,9 +25,9 @@ class TransactionListAdapter( private val amountTextView: TextView = binding.amountText private val titleTextView: TextView = binding.titleText private val locationTextView: TextView = binding.transactionLocationText + private val transactionParser = TransactionParser() fun bind(transaction: Transaction) { - val transactionParser = TransactionParser() iconImageView.setImageResource( if (transaction.category == TransactionCategory.EARNINGS) { R.drawable.ic_coins diff --git a/app/src/main/java/com/example/bondoman/views/components/DateInputComponent.kt b/app/src/main/java/com/example/bondoman/views/components/DateInputComponent.kt index d6308964759c1e9a95a54bcba46bd663e3a5e45f..8e744cc8bc7a6e0aacf73ef23d1f7bb73625f46e 100644 --- a/app/src/main/java/com/example/bondoman/views/components/DateInputComponent.kt +++ b/app/src/main/java/com/example/bondoman/views/components/DateInputComponent.kt @@ -42,7 +42,7 @@ class DateInputComponent button.setOnClickListener { val datePickerFragment = DatePickerFragment() datePickerFragment.setOnDatePicked { year, month, day -> - setText("$year/${month + 1}/$day") + setText(year, month, day) onDatePickedListener?.invoke(year, month, day) } datePickerFragment.show(context.supportFragmentManager, FRAGMENT_TAG) @@ -50,10 +50,18 @@ class DateInputComponent } } - fun setText(text: String) { + private fun setText(text: String) { button.text = text } + fun setText( + year: Int, + month: Int, + day: Int, + ) { + setText("$year/${month + 1}/$day") + } + fun setOnDatePicked(listener: (year: Int, month: Int, day: Int) -> Unit) { onDatePickedListener = listener } diff --git a/app/src/main/java/com/example/bondoman/views/components/TextInputComponent.kt b/app/src/main/java/com/example/bondoman/views/components/TextInputComponent.kt index 7ea5445efdfdcf06ab00616661642aef15e48952..b48dee18ac8a2e3c3fb98dd3c2e4d29db0bf81c5 100644 --- a/app/src/main/java/com/example/bondoman/views/components/TextInputComponent.kt +++ b/app/src/main/java/com/example/bondoman/views/components/TextInputComponent.kt @@ -15,134 +15,200 @@ import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textview.MaterialTextView class TextInputComponent - @JvmOverloads - constructor( - context: Context, - attrs: AttributeSet? = null, - defStyleAttr: Int = 0, - ) : LinearLayout(context, attrs, defStyleAttr) { - private val labelTextView: MaterialTextView - private val inputEditText: TextInputEditText - private var beforeTextChangedListener: ((text: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = - null - private var onTextChangedListener: ((text: CharSequence?, start: Int, before: Int, count: Int) -> Unit)? = - null - private var afterTextChangedListener: ((text: Editable?) -> Unit)? = null - - init { - LayoutInflater.from(context).inflate(R.layout.component_text_input, this, true) - - labelTextView = findViewById(R.id.text_input_component_label) - inputEditText = findViewById(R.id.text_input_component_field) - - // Load attributes - val attributes = context.obtainStyledAttributes(attrs, R.styleable.TextInputComponent) - val labelText = attributes.getString(R.styleable.TextInputComponent_textInputLabel) ?: "" - val hint = attributes.getString(R.styleable.TextInputComponent_textInputHint) ?: "" - val inputType = - attrs?.getAttributeIntValue( - "http://schemas.android.com/apk/res/android", - "inputType", - InputType.TYPE_CLASS_TEXT, - ) ?: InputType.TYPE_CLASS_TEXT - - val digits = - attrs?.getAttributeValue( - "http://schemas.android.com/apk/res/android", - "digits", - ) - - val maxLength = - attrs?.getAttributeValue( - "http://schemas.android.com/apk/res/android", - "maxLength", - ) - - attributes.recycle() - - // Set attributes - labelTextView.text = labelText - inputEditText.hint = hint - inputEditText.inputType = inputType - - if (digits != null) { - inputEditText.keyListener = DigitsKeyListener.getInstance(digits) - } +@JvmOverloads +constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : LinearLayout(context, attrs, defStyleAttr) { + private val labelTextView: MaterialTextView + private val inputEditText: TextInputEditText + private var beforeTextChangedListener: ((text: CharSequence?, start: Int, count: Int, after: Int) -> Unit)? = + null + private var onTextChangedListener: ((text: CharSequence?, start: Int, before: Int, count: Int) -> Unit)? = + null + private var afterTextChangedListener: ((text: Editable?) -> Unit)? = null + + companion object { + enum class Keyboard(val value: Int) { + TEXT(0), + NUMBER(1), + } + } - if (maxLength != null) { - val filterArray = arrayOfNulls<InputFilter>(1) - filterArray[0] = LengthFilter(maxLength.toInt()) - inputEditText.setFilters(filterArray) - } + init { + LayoutInflater.from(context).inflate(R.layout.component_text_input, this, true) + + labelTextView = findViewById(R.id.text_input_component_label) + inputEditText = findViewById(R.id.text_input_component_field) + + // Load attributes + val attributes = context.obtainStyledAttributes(attrs, R.styleable.TextInputComponent) + val labelText = attributes.getString(R.styleable.TextInputComponent_textInputLabel) ?: "" + val hint = attributes.getString(R.styleable.TextInputComponent_textInputHint) ?: "" + val inputType = + attrs?.getAttributeIntValue( + "http://schemas.android.com/apk/res/android", + "inputType", + InputType.TYPE_CLASS_TEXT, + ) ?: InputType.TYPE_CLASS_TEXT + + val digits = + attrs?.getAttributeValue( + "http://schemas.android.com/apk/res/android", + "digits", + ) - // Set up text watcher - inputEditText.addTextChangedListener( - object : TextWatcher { - override fun beforeTextChanged( - s: CharSequence?, - start: Int, - count: Int, - after: Int, - ) { - beforeTextChangedListener?.invoke(s, start, count, after) - } - - override fun onTextChanged( - s: CharSequence?, - start: Int, - before: Int, - count: Int, - ) { - onTextChangedListener?.invoke(s, start, before, count) - } - - override fun afterTextChanged(s: Editable?) { - afterTextChangedListener?.invoke(s) - } - }, + val maxLength = + attrs?.getAttributeValue( + "http://schemas.android.com/apk/res/android", + "maxLength", ) - } - fun setInputType(type: Int) { - inputEditText.inputType = type - } + val keyboardOrdinal = + attributes.getInt( + R.styleable.TextInputComponent_textInputKeyboard, + TextInputComponent.Companion.Keyboard.TEXT.ordinal, + ) - fun setText(text: String) { - inputEditText.setText(text) - } - fun getText(): String { - return inputEditText.text.toString() + attributes.recycle() + + // Set attributes + labelTextView.text = labelText + inputEditText.hint = hint + inputEditText.inputType = inputType + + if (digits != null) { + when (keyboardOrdinal) { + Keyboard.TEXT.ordinal -> { + val textWatcher = + object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int, + ) { + // This method is called before the text is changed + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int, + ) { + if (s != null) { + val filteredText = + s.filter { + it in digits + } + + if (filteredText.length < s.length) { + inputEditText.setText(filteredText) + inputEditText.setSelection( + filteredText.length, + ) + } + } + } + + override fun afterTextChanged(s: Editable?) { + // This method is called after the text has changed + } + } + + // Attach the TextWatcher to the EditText + inputEditText.addTextChangedListener(textWatcher) + } + + Keyboard.NUMBER.ordinal -> { + inputEditText.keyListener = DigitsKeyListener.getInstance(digits) + } + } } - fun disable() { - inputEditText.isCursorVisible = false - inputEditText.isFocusableInTouchMode = false + if (maxLength != null) { + val filterArray = arrayOfNulls<InputFilter>(1) + filterArray[0] = LengthFilter(maxLength.toInt()) + inputEditText.setFilters(filterArray) } - fun enable() { - inputEditText.isCursorVisible = true - inputEditText.isFocusableInTouchMode = true + if (maxLength != null) { + val filterArray = arrayOfNulls<InputFilter>(1) + filterArray[0] = LengthFilter(maxLength.toInt()) + inputEditText.setFilters(filterArray) } - // Setter methods for text change listeners - fun setBeforeTextChangedListener(listener: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit) { - beforeTextChangedListener = listener - } + // Set up text watcher + inputEditText.addTextChangedListener( + object : TextWatcher { + override fun beforeTextChanged( + s: CharSequence?, + start: Int, + count: Int, + after: Int, + ) { + beforeTextChangedListener?.invoke(s, start, count, after) + } + + override fun onTextChanged( + s: CharSequence?, + start: Int, + before: Int, + count: Int, + ) { + onTextChangedListener?.invoke(s, start, before, count) + } + + override fun afterTextChanged(s: Editable?) { + afterTextChangedListener?.invoke(s) + } + }, + ) + } - fun setOnTextChangedListener(listener: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) { - onTextChangedListener = listener - } + fun setInputType(type: Int) { + inputEditText.inputType = type + } - fun setAfterTextChangedListener(listener: (text: Editable?) -> Unit) { - afterTextChangedListener = listener - } + fun setText(text: String) { + inputEditText.setText(text) + } - fun addTextWactcher(textWatcher: TextWatcher) { - inputEditText.addTextChangedListener(textWatcher) - } + fun getText(): String { + return inputEditText.text.toString() + } - fun removeTextWatcher(textWatcher: TextWatcher) { - inputEditText.removeTextChangedListener(textWatcher) - } + fun disable() { + inputEditText.isCursorVisible = false + inputEditText.isFocusableInTouchMode = false + } + + fun enable() { + inputEditText.isCursorVisible = true + inputEditText.isFocusableInTouchMode = true + } + + // Setter methods for text change listeners + fun setBeforeTextChangedListener(listener: (text: CharSequence?, start: Int, count: Int, after: Int) -> Unit) { + beforeTextChangedListener = listener + } + + fun setOnTextChangedListener(listener: (text: CharSequence?, start: Int, before: Int, count: Int) -> Unit) { + onTextChangedListener = listener + } + + fun setAfterTextChangedListener(listener: (text: Editable?) -> Unit) { + afterTextChangedListener = listener + } + + fun addTextWactcher(textWatcher: TextWatcher) { + inputEditText.addTextChangedListener(textWatcher) + } + + fun removeTextWatcher(textWatcher: TextWatcher) { + inputEditText.removeTextChangedListener(textWatcher) } +} diff --git a/app/src/main/java/com/example/bondoman/views/fragments/ConnectionLostFragment.kt b/app/src/main/java/com/example/bondoman/views/fragments/ConnectionLostFragment.kt new file mode 100644 index 0000000000000000000000000000000000000000..88faba1482d650b4c7cc0e7dc04f49805979ac14 --- /dev/null +++ b/app/src/main/java/com/example/bondoman/views/fragments/ConnectionLostFragment.kt @@ -0,0 +1,31 @@ +package com.example.bondoman.views.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import com.example.bondoman.databinding.FragmentConnectionLostBinding + +class ConnectionLostFragment : Fragment() { + + companion object { + @JvmStatic + fun newInstance() = + ConnectionLostFragment().apply { + } + } + + private lateinit var binding: FragmentConnectionLostBinding + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + // Inflate the layout for this fragment + binding = FragmentConnectionLostBinding.inflate(inflater) + return binding.root + } + +} diff --git a/app/src/main/java/com/example/bondoman/views/fragments/GraphFragment.kt b/app/src/main/java/com/example/bondoman/views/fragments/GraphFragment.kt index acb9f6d867c6d0b22d50cfeed3e4cf7e7418cc93..e039061067dae4d62bf76c41b01d067656f85c3a 100644 --- a/app/src/main/java/com/example/bondoman/views/fragments/GraphFragment.kt +++ b/app/src/main/java/com/example/bondoman/views/fragments/GraphFragment.kt @@ -5,8 +5,14 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat +import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider import com.example.bondoman.R +import com.example.bondoman.data.models.TransactionCategory +import com.example.bondoman.data.utils.TransactionParser +import com.example.bondoman.data.viewmodels.transaction.TransactionViewModel +import com.example.bondoman.databinding.FragmentGraphBinding import com.github.mikephil.charting.animation.Easing import com.github.mikephil.charting.charts.PieChart import com.github.mikephil.charting.data.PieData @@ -14,6 +20,8 @@ import com.github.mikephil.charting.data.PieDataSet import com.github.mikephil.charting.data.PieEntry import com.github.mikephil.charting.formatter.PercentFormatter import java.util.Arrays +import java.util.Calendar +import java.util.Date class GraphFragment : Fragment() { companion object { @@ -23,7 +31,14 @@ class GraphFragment : Fragment() { } } + private lateinit var viewBinding: FragmentGraphBinding private lateinit var pieChart: PieChart + private lateinit var transactionViewModel: TransactionViewModel + + private lateinit var beginDate: Date + private lateinit var endDate: Date + + private val transactionParser = TransactionParser() private fun initPieChart() { // using percentage as values instead of amount @@ -49,16 +64,18 @@ class GraphFragment : Fragment() { // adding animation so the entries pop up from 0 degree pieChart.animateY(1400, Easing.EaseInOutQuad) + + // adding extra offset for avoiding cut-off + pieChart.extraTopOffset = 10f + pieChart.extraBottomOffset = 10f + + pieChart.holeRadius = 30f + pieChart.transparentCircleRadius = 40f } - private fun showPieChart() { + private fun showPieChart(typeAmountMap: MutableMap<String, Double>) { val pieEntries = ArrayList<PieEntry>() - // initializing data - val typeAmountMap: MutableMap<String, Int> = HashMap() - typeAmountMap["Income"] = 200 - typeAmountMap["Outcome"] = 100 - // initializing colors for the entries val colorArray = Arrays.copyOfRange( @@ -97,7 +114,7 @@ class GraphFragment : Fragment() { // set line for values pieDataSet.yValuePosition = PieDataSet.ValuePosition.OUTSIDE_SLICE - pieDataSet.valueLinePart1Length = 0.5f + pieDataSet.valueLinePart1Length = 0.7f pieDataSet.valueLinePart2Length = 0.3f pieDataSet.valueLinePart1OffsetPercentage = 95f pieDataSet.valueLineColor = ContextCompat.getColor(requireContext(), R.color.zinc_300) @@ -114,22 +131,149 @@ class GraphFragment : Fragment() { // showing the value of the entries, default true if not set pieData.setDrawValues(true) pieChart.setData(pieData) + pieChart.notifyDataSetChanged() pieChart.invalidate() } + private fun setBeginDate( + year: Int, + month: Int, + day: Int, + ) { + val calendar = Calendar.getInstance() + calendar.set(year, month, day, 0, 0, 0) + + beginDate = calendar.time + } + + private fun setEndDate( + year: Int, + month: Int, + day: Int, + ) { + val calendar = Calendar.getInstance() + calendar.set(year, month, day, 0, 0, 0) + calendar.add(Calendar.DAY_OF_YEAR, 1) + + endDate = calendar.time + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + transactionViewModel = + ViewModelProvider(requireActivity())[TransactionViewModel::class.java] + } + + private fun configureObserver() { + transactionViewModel.categoryTotals.observe( + viewLifecycleOwner, + ) { + if (it != null) { + val typeAmountMap: MutableMap<String, Double> = HashMap() + + for (categoryTotal in it) { + typeAmountMap[categoryTotal.category.string] = categoryTotal.totalAmount + } + + for (category in TransactionCategory.entries) { + // Check if the map contains the key for the current category + if (!typeAmountMap.containsKey(category.string)) { + // If the key is missing, assign 0 as the value + typeAmountMap[category.string] = 0.0 + } + } + + viewBinding.incomeValueText.text = + transactionParser.getAmountString( + typeAmountMap[TransactionCategory.EARNINGS.string]!!, + TransactionCategory.EARNINGS, + ) + viewBinding.outcomeValueText.text = + transactionParser.getAmountString( + typeAmountMap[TransactionCategory.EXPENSE.string]!!, + TransactionCategory.EXPENSE, + ) + + if (it.isNotEmpty()) { + showPieChart(typeAmountMap) + viewBinding.graphPieChart.isVisible = true + viewBinding.graphPageInfoNotAvailable.isVisible = false + } else { + viewBinding.graphPageInfoNotAvailable.isVisible = true + viewBinding.graphPieChart.isVisible = false + } + } + } + } + + private fun configureInitialDate() { + val calendar = Calendar.getInstance() + calendar.time = Date() + + // Get the first day of the current month + calendar.set(Calendar.DAY_OF_MONTH, 1) + beginDate = calendar.time + + var year = calendar.get(Calendar.YEAR) + var month = calendar.get(Calendar.MONTH) + var day = calendar.get(Calendar.DAY_OF_MONTH) + setBeginDate(year, month, day) + viewBinding.graphDateBeginInput.setText(year, month, day) + + calendar.set(Calendar.DAY_OF_MONTH, calendar.getActualMaximum(Calendar.DAY_OF_MONTH)) + endDate = calendar.time + + year = calendar.get(Calendar.YEAR) + month = calendar.get(Calendar.MONTH) + day = calendar.get(Calendar.DAY_OF_MONTH) + setEndDate(year, month, day) + viewBinding.graphDateEndInput.setText(year, month, day) + } + + private fun configureDateInput() { + val dateBeginInput = viewBinding.graphDateBeginInput + val dateEndInput = viewBinding.graphDateEndInput + + dateBeginInput.setOnDatePicked { year, month, day -> + setBeginDate(year, month, day) + + transactionViewModel.getTransactionTotals(beginDate, endDate) + } + + dateEndInput.setOnDatePicked { year, month, day -> + setEndDate(year, month, day) + + transactionViewModel.getTransactionTotals(beginDate, endDate) + } + } + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?, - ): View? { + ): View { // Inflate the layout for this fragment - val view = inflater.inflate(R.layout.fragment_graph, container, false) + viewBinding = FragmentGraphBinding.inflate(inflater, container, false) // configure pie chart - pieChart = view.findViewById(R.id.graph_pie_chart) + pieChart = viewBinding.graphPieChart initPieChart() - showPieChart() - return view + // configure initial value + viewBinding.incomeValueText.text = "-" + viewBinding.outcomeValueText.text = "-" + + configureObserver() + configureInitialDate() + configureDateInput() + + return viewBinding.root + } + + override fun onStart() { + super.onStart() + + transactionViewModel.getTransactionTotals(beginDate, endDate) } } diff --git a/app/src/main/java/com/example/bondoman/views/fragments/TransactionFormFragment.kt b/app/src/main/java/com/example/bondoman/views/fragments/TransactionFormFragment.kt index 0118facc90601f8a60b85055a8bb00c8ce1632c6..02035bdbd9e7fa0155e442a9e131d1c9dd0f9034 100644 --- a/app/src/main/java/com/example/bondoman/views/fragments/TransactionFormFragment.kt +++ b/app/src/main/java/com/example/bondoman/views/fragments/TransactionFormFragment.kt @@ -19,6 +19,7 @@ import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import com.example.bondoman.R import com.example.bondoman.data.models.TransactionCategory @@ -28,6 +29,9 @@ import com.example.bondoman.databinding.FragmentTransactionFormBinding import com.example.bondoman.views.activities.MainActivity import com.example.bondoman.views.utils.interfaces.ParentActivityService import com.example.bondoman.views.utils.interfaces.SecondaryFragment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.util.Date import java.util.Locale import java.util.function.Consumer @@ -93,6 +97,7 @@ abstract class TransactionFormFragment : Fragment(), SecondaryFragment { object : LocationListener { override fun onLocationChanged(location: Location) { currentGpsLocation = location + changeLocationData() } override fun onStatusChanged( @@ -111,6 +116,7 @@ abstract class TransactionFormFragment : Fragment(), SecondaryFragment { object : LocationListener { override fun onLocationChanged(location: Location) { currentNetworkLocation = location + changeLocationData() } override fun onStatusChanged( @@ -178,6 +184,7 @@ abstract class TransactionFormFragment : Fragment(), SecondaryFragment { ArrayAdapter(requireContext(), R.layout.component_dropdown_item, categories) autoCompleteTextView.setAdapter(dropdownAdapter) + autoCompleteTextView.setDropDownBackgroundResource(R.color.bg_main) autoCompleteTextView.onFocusChangeListener = View.OnFocusChangeListener { _, hasFocus -> if (!hasFocus) { @@ -228,20 +235,27 @@ abstract class TransactionFormFragment : Fragment(), SecondaryFragment { } } - private fun getLocationString(location: Location): String? { - val geocoder = Geocoder(requireContext(), Locale.getDefault()) - val addresses = - geocoder.getFromLocation( - location.latitude, - location.longitude, - 1, - ) + private suspend fun getLocationStringSuspend( + location: Location, + callback: (String?) -> Unit, + ) { + withContext(Dispatchers.IO) { + val geocoder = Geocoder(requireContext(), Locale.getDefault()) + val addresses = + geocoder.getFromLocation( + location.latitude, + location.longitude, + 1, + ) - return if (!addresses.isNullOrEmpty()) { - val address = addresses[0] - address.getAddressLine(0) - } else { - null + if (!addresses.isNullOrEmpty()) { + val address = addresses[0] + val locationString = address.getAddressLine(0) + + callback(locationString) + } else { + callback(null) + } } } @@ -269,9 +283,12 @@ abstract class TransactionFormFragment : Fragment(), SecondaryFragment { locationTextString.postValue(it) } } else { - val locationString = getLocationString(currentLocation!!) - location = locationString - locationTextString.value = locationString + lifecycleScope.launch { + getLocationStringSuspend(currentLocation!!) { + location = it + locationTextString.postValue(it) + } + } } } diff --git a/app/src/main/java/com/example/bondoman/views/fragments/TransactionListFragment.kt b/app/src/main/java/com/example/bondoman/views/fragments/TransactionListFragment.kt index 6f1584fa3ff3964dd663f16e3a218601c4782863..9366a9b559728f04276b525216141f7b96d481eb 100644 --- a/app/src/main/java/com/example/bondoman/views/fragments/TransactionListFragment.kt +++ b/app/src/main/java/com/example/bondoman/views/fragments/TransactionListFragment.kt @@ -38,7 +38,7 @@ class TransactionListFragment : Fragment(), TransactionClickListener { adapter = TransactionListAdapter(this) recyclerView.adapter = adapter - transactionViewModel.transactions.observe(requireActivity()) { + transactionViewModel.transactions.observe(viewLifecycleOwner) { adapter.submitList(it) viewBinding.transactionListPageInfoWaiting.visibility = View.GONE @@ -109,7 +109,7 @@ class TransactionListFragment : Fragment(), TransactionClickListener { } transactionViewModel.currentTransaction.observe( - requireActivity(), + viewLifecycleOwner, currentTransactionObserver, ) detailLayout.setUpdateListener { diff --git a/app/src/main/java/com/example/bondoman/views/utils/filters/CustomInputFilter.kt b/app/src/main/java/com/example/bondoman/views/utils/filters/CustomInputFilter.kt new file mode 100644 index 0000000000000000000000000000000000000000..7d478f5b62d8d1b7d2e1b4607e7b55fc179d829e --- /dev/null +++ b/app/src/main/java/com/example/bondoman/views/utils/filters/CustomInputFilter.kt @@ -0,0 +1,29 @@ +package com.example.bondoman.views.utils.filters + +import android.text.InputFilter +import android.text.Spanned + +class CustomInputFilter(private val allowedCharacters: String) : InputFilter { + override fun filter( + source: CharSequence?, + start: Int, + end: Int, + dest: Spanned?, + dstart: Int, + dend: Int, + ): CharSequence? { + val builder = StringBuilder() + + // Loop through each character in the source + for (i in start until end) { + val c = source?.get(i) + + if (c != null && allowedCharacters.contains(c, ignoreCase = true)) { + builder.append(c) + } + } + + val allCharactersValid = builder.length == end - start + return if (allCharactersValid) null else builder.toString() + } +} diff --git a/app/src/main/res/layout/component_dropdown_item.xml b/app/src/main/res/layout/component_dropdown_item.xml index 8a1035c55823e3f7a138787311a6fcb7999a72dd..a7d4c0cca40d4f3c3e7820955f7cfd0d6595ac80 100644 --- a/app/src/main/res/layout/component_dropdown_item.xml +++ b/app/src/main/res/layout/component_dropdown_item.xml @@ -1,11 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> -<TextView - xmlns:android="http://schemas.android.com/apk/res/android" +<TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/textViewFeelings" android:layout_width="match_parent" android:layout_height="39dp" - android:text="@string/dummy_dropdown_item" - android:theme="@style/TextAppearance.TransactionForm.Input" android:background="@color/bg_main" - android:paddingLeft="10dp" - android:gravity="center_vertical"/> \ No newline at end of file + android:gravity="center_vertical" + android:paddingHorizontal="10dp" + android:text="@string/dummy_dropdown_item" + android:theme="@style/TextAppearance.TransactionForm.Input" /> diff --git a/app/src/main/res/layout/fragment_connection_lost.xml b/app/src/main/res/layout/fragment_connection_lost.xml new file mode 100644 index 0000000000000000000000000000000000000000..d6c8b33a02f7787ace5ff6ebd51d9561f9379d9f --- /dev/null +++ b/app/src/main/res/layout/fragment_connection_lost.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.example.bondoman.views.components.PageInfoComponent xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/graph_page_info_not_available" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:src="@drawable/ic_router_off" + app:pageInfoText="@string/text_page_info_connection_lost" /> diff --git a/app/src/main/res/layout/fragment_graph.xml b/app/src/main/res/layout/fragment_graph.xml index 05f24c55b0d326da90711fb382cedba706cb7a38..b6697d50be0cca80d4c013b620622d21c5756380 100644 --- a/app/src/main/res/layout/fragment_graph.xml +++ b/app/src/main/res/layout/fragment_graph.xml @@ -31,12 +31,12 @@ android:src="@drawable/ic_minus" app:layout_constraintBottom_toTopOf="@id/graph_guideline_1" app:layout_constraintLeft_toRightOf="@id/graph_date_begin_input" - app:layout_constraintRight_toLeftOf="@id/graph_end_begin_input" + app:layout_constraintRight_toLeftOf="@id/graph_date_end_input" app:layout_constraintTop_toTopOf="parent" app:tint="@color/zinc_300" /> <com.example.bondoman.views.components.DateInputComponent - android:id="@+id/graph_end_begin_input" + android:id="@+id/graph_date_end_input" android:layout_width="0dp" android:layout_height="wrap_content" app:dateInputLabel="@string/text_date_end_label" @@ -55,6 +55,17 @@ app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/graph_guideline_2" /> + <com.example.bondoman.views.components.PageInfoComponent + android:id="@+id/graph_page_info_not_available" + android:layout_width="0dp" + android:layout_height="0dp" + android:src="@drawable/ic_chart_pie" + app:layout_constraintBottom_toTopOf="@id/graph_guideline_3" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/graph_guideline_2" + app:pageInfoText="@string/text_page_info_graph_not_available" /> + <TextView android:layout_width="wrap_content" android:layout_height="19dp" @@ -70,7 +81,7 @@ app:layout_constraintTop_toBottomOf="@id/graph_guideline_3" /> <TextView - + android:id="@+id/income_value_text" android:layout_width="0dp" android:layout_height="wrap_content" android:ellipsize="end" @@ -98,6 +109,7 @@ app:layout_constraintTop_toBottomOf="@id/graph_guideline_4" /> <TextView + android:id="@+id/outcome_value_text" android:layout_width="0dp" android:layout_height="wrap_content" android:ellipsize="end" @@ -129,21 +141,21 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_begin="430dp" /> + app:layout_constraintGuide_begin="450dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/graph_guideline_4" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_begin="489dp" /> + app:layout_constraintGuide_begin="509dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/graph_guideline_5" android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal" - app:layout_constraintGuide_begin="548dp" /> + app:layout_constraintGuide_begin="568dp" /> <androidx.constraintlayout.widget.Guideline android:id="@+id/graph_guideline_6" diff --git a/app/src/main/res/layout/fragment_transaction_form.xml b/app/src/main/res/layout/fragment_transaction_form.xml index 236fa79a2f0843421afff401c152eb1b610583f7..f9a587057853e61dcae2c0dfd843a55eb092efd2 100644 --- a/app/src/main/res/layout/fragment_transaction_form.xml +++ b/app/src/main/res/layout/fragment_transaction_form.xml @@ -14,6 +14,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/bg_full_border" + android:textColorHint="@color/zinc_300" android:theme="@style/transactionFormFieldBackground" app:boxStrokeColor="@color/teal_200" app:cursorColor="@color/teal_200" @@ -29,7 +30,8 @@ android:hint="@string/dummy_category_dropdown_placeholder" android:inputType="none" android:minHeight="44dp" - android:textAppearance="@style/TextAppearance.TransactionForm.Input" /> + android:textAppearance="@style/TextAppearance.TransactionForm.Input" + android:textColor="@color/zinc_300" /> </com.google.android.material.textfield.TextInputLayout> <com.example.bondoman.views.components.TextInputComponent @@ -42,6 +44,8 @@ android:id="@+id/transaction_form_title" android:layout_width="match_parent" android:layout_height="wrap_content" + android:digits="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 " + android:inputType="text" android:maxLength="50" app:textInputHint="@string/hint_transaction_form_title" app:textInputLabel="Title" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index c7b25496f81345d94fd5721a9ab10429b604756b..7e7cd9d9f736d99c660618bf8747a86bdef13a31 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -5,6 +5,7 @@ android:id="@+id/bondoman_nav" app:startDestination="@id/settingsFragment"> + <fragment android:id="@+id/twibbonPreviewFragment" android:name="com.example.bondoman.views.fragments.TwibbonPreviewFragment" @@ -78,6 +79,10 @@ android:name="com.example.bondoman.views.fragments.GraphFragment" android:label="@string/menu_title_charts" tools:layout="@layout/fragment_graph" /> + <fragment + android:id="@+id/ConnectionLostFragment" + android:name="com.example.bondoman.views.fragments.ConnectionLostFragment" + tools:layout="@layout/fragment_connection_lost" /> <fragment android:id="@+id/scanReceiptResultFragment" android:name="com.example.bondoman.views.fragments.ScanReceiptResultFragment" @@ -90,4 +95,7 @@ android:name="ScanReceiptImageBitmap" app:argType="com.example.bondoman.data.models.ParcelableBitmap" /> </fragment> + <action + android:id="@+id/action_global_to_ConnectionLostFragment" + app:destination="@id/ConnectionLostFragment" /> </navigation> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4df73cc850735422dd5d6cf5b8af2682c5c67709..6e9bd6e926f94f53f7fbf007339868f3050686c3 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -18,6 +18,10 @@ <attr name="textInputLabel" format="string" /> <attr name="textInputHint" format="string" /> <attr name="textInputType" format="string" /> + <attr name="textInputKeyboard" format="enum"> + <enum name="text" value="0" /> + <enum name="number" value="1" /> + </attr> <attr name="android:inputType" /> <attr name="android:digits" /> <attr name="android:maxLength" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b3016b96524c522831002080ae4ef14cbdddbb2..25142fc9b0c03f244b7727569643698ac776fcf2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -66,6 +66,9 @@ <string name="dummy_page_info_text">This is just an <b>example</b>. Please customize this text.</string> <string name="text_page_info_getting_transactions">Getting transactions…</string> <string name="text_page_info_empty_transaction">Transaction is <b>empty</b>. Please add new transaction(s)</string> + <string name="text_page_info_graph_not_available">Pie chart is <b>not</b> available for current data</string> + <string name="text_page_info_connection_lost">You\'re <b>offline</b>. Please check your internet connection</string> + <string name="menu_title_connection_lost">Connection Lost</string> <string name="scan_receipt_image_result">Scan receipt image result</string> <string name="scan_receipt_form_button_retry_label">Retry</string> <string name="scan_receipt_form_button_save_label">Save</string>