Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
No results found
Show changes
Commits on Source (13)
Showing
with 716 additions and 27 deletions
......@@ -34,6 +34,12 @@ android {
buildFeatures {
viewBinding = true
}
packaging {
resources {
excludes += "META-INF/*"
}
}
}
dependencies {
......@@ -47,6 +53,7 @@ dependencies {
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
implementation(libs.androidx.annotation)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
......@@ -57,4 +64,16 @@ dependencies {
implementation(libs.room.common)
annotationProcessor(libs.room.compiler)
kapt(libs.room.compiler)
// Retrofit
implementation(libs.retrofit2.retrofit)
implementation(libs.retrofit2.converter.gson)
implementation(libs.retrofit2.converter.scalars)
implementation(libs.okhttp3)
// Work
implementation(libs.androidx.work.runtime)
// Excel
implementation(libs.excelkt)
}
\ No newline at end of file
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools" >
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<application
android:allowBackup="true"
......@@ -10,11 +15,19 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Bondoman"
>
android:theme="@style/Theme.Bondoman" >
<activity
android:name=".NoInternetActivity"
android:exported="false"
android:noHistory="true"
android:theme="@style/Theme.Bondoman" />
<activity
android:name=".ui.login.LoginActivity"
android:exported="false"
android:label="@string/title_activity_login"/>
<activity
android:name=".MainActivity"
android:exported="true">
android:exported="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
......@@ -23,7 +36,17 @@
</activity>
<activity
android:name=".AddTransactionActivity"
android:exported="false"/>
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
\ No newline at end of file
package com.onionsquad.bondoman
import android.content.Context
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Bundle
import android.os.PersistableBundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
open class InternetActivity : AppCompatActivity() {
var isInternetConnected = false
private set
private val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
isInternetConnected = true
}
override fun onLost(network: Network) {
super.onLost(network)
isInternetConnected = false
Toast.makeText(
this@InternetActivity,
R.string.internet_not_connected,
Toast.LENGTH_SHORT
).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
connectivityManager.requestNetwork(networkRequest, networkCallback)
}
}
\ No newline at end of file
package com.onionsquad.bondoman
import android.content.Intent
import android.os.Bundle
import com.google.android.material.bottomnavigation.BottomNavigationView
import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.onionsquad.bondoman.auth.AutoLogoutWorker
import com.onionsquad.bondoman.auth.SessionManager
import com.onionsquad.bondoman.databinding.ActivityMainBinding
import com.onionsquad.bondoman.ui.login.LoginActivity
class MainActivity : AppCompatActivity() {
class MainActivity : InternetActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AutoLogoutWorker.start(this)
val sessionManager = SessionManager(this)
sessionManager.ensureAuthenticated()
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
......
package com.onionsquad.bondoman
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.onionsquad.bondoman.databinding.ActivityNoInternetBinding
class NoInternetActivity : AppCompatActivity() {
private lateinit var binding: ActivityNoInternetBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityNoInternetBinding.inflate(layoutInflater)
setContentView(binding.root)
}
}
\ No newline at end of file
package com.onionsquad.bondoman.auth
import android.content.Context
import android.util.Log
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.time.Instant
import java.util.concurrent.TimeUnit
import kotlin.math.max
class AutoLogoutWorker(
private val context: Context,
private val workerParameters: WorkerParameters
) : Worker(context, workerParameters) {
override fun doWork(): Result {
Log.d(this::class.java.simpleName, "Proceed auto logout")
val sessionManager = SessionManager(context)
sessionManager.deleteAuthToken()
return Result.success()
}
companion object {
fun start(context: Context) {
Log.d(AutoLogoutWorker::class.java.simpleName, "Starting auto logout worker")
val sessionManager = SessionManager(context)
val token = sessionManager.fetchAuthToken()
if (token != null) {
val duration = max(0, token.exp - Instant.now().epochSecond)
Log.d(AutoLogoutWorker::class.java.simpleName, "Proceed to logout in $duration secs")
val workRequest = OneTimeWorkRequestBuilder<AutoLogoutWorker>()
.setInitialDelay(duration, TimeUnit.SECONDS)
.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(
AutoLogoutWorker::class.java.simpleName,
ExistingWorkPolicy.REPLACE,
workRequest
)
} else {
stop(context)
}
}
fun stop(context: Context) {
Log.d(AutoLogoutWorker::class.java.simpleName, "Stopping auto logout worker")
WorkManager
.getInstance(context)
.cancelUniqueWork(AutoLogoutWorker::class.java.simpleName)
}
}
}
\ No newline at end of file
package com.onionsquad.bondoman.auth
import android.content.Context
import android.content.Intent
import android.util.Log
import com.google.gson.Gson
import com.onionsquad.bondoman.R
import com.onionsquad.bondoman.ui.login.LoginActivity
class SessionManager(private val context: Context) {
private val sharedPreferences = context.getSharedPreferences(
context.getString(R.string.preference_file_key),
Context.MODE_PRIVATE
)
companion object {
const val USER_TOKEN = "user_token"
}
fun fetchAuthToken(): Token? {
val tokenJson = sharedPreferences.getString(USER_TOKEN, null)
return if (tokenJson != null) Gson().fromJson(tokenJson, Token::class.java) else null
}
fun saveAuthToken(token: Token) {
sharedPreferences.edit().putString(USER_TOKEN, Gson().toJson(token)).apply()
Log.d("TOKEN", "${fetchAuthToken()}")
}
fun deleteAuthToken() {
sharedPreferences.edit().remove(USER_TOKEN).apply()
}
fun ensureAuthenticated() {
if (fetchAuthToken() == null) {
val intent = Intent(context, LoginActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
context.startActivity(intent)
}
}
}
\ No newline at end of file
package com.onionsquad.bondoman.auth
data class Token(val tokenString: String, val nim: String, val exp: Long, val iat: Long)
package com.onionsquad.bondoman.network
import okhttp3.OkHttpClient
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.converter.scalars.ScalarsConverterFactory
import retrofit2.http.Body
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.POST
private const val BASE_URL = "https://pbd-backend-2024.vercel.app/"
private val client = OkHttpClient.Builder().build()
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build()
interface BondomanApiService {
@Headers("Content-Type: application/json")
@POST("api/auth/login")
fun login(@Body loginData: LoginRequest): Call<String>
@POST("api/auth/token")
fun token(@Header("Authorization") token: String): Call<TokenResponse>
}
object BondomanApi {
val retrofitService: BondomanApiService by lazy {
retrofit.create(BondomanApiService::class.java)
}
}
\ No newline at end of file
package com.onionsquad.bondoman.network
data class LoginRequest(val email: String?, val password: String?)
\ No newline at end of file
package com.onionsquad.bondoman.network
import com.google.gson.annotations.SerializedName
data class LoginResponse(val token: String?)
package com.onionsquad.bondoman.network
data class TokenResponse(val nim: String?, val exp: Long?, val iat: Long?)
......@@ -2,5 +2,6 @@ package com.onionsquad.bondoman.room
enum class TransactionCategory {
INCOME,
OUTCOME
OUTCOME,
UNKNOWN
}
\ No newline at end of file
package com.onionsquad.bondoman.ui.login
import android.app.Activity
import android.content.Intent
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.text.Editable
import android.text.TextWatcher
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import android.widget.Toast
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.onionsquad.bondoman.InternetActivity
import com.onionsquad.bondoman.MainActivity
import com.onionsquad.bondoman.NoInternetActivity
import com.onionsquad.bondoman.databinding.ActivityLoginBinding
import com.onionsquad.bondoman.R
import com.onionsquad.bondoman.auth.AutoLogoutWorker
import com.onionsquad.bondoman.auth.SessionManager
import java.time.Instant
import java.util.concurrent.TimeUnit
class LoginActivity : InternetActivity() {
private lateinit var loginViewModel: LoginViewModel
private lateinit var binding: ActivityLoginBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding.root)
val email = binding.username
val password = binding.password
val login = binding.login
val loading = binding.loading
val sessionManager = SessionManager(this)
loginViewModel = ViewModelProvider(this)[LoginViewModel::class.java]
.apply {
loginFormState.observe(this@LoginActivity, Observer {
val loginState = it ?: return@Observer
login.isEnabled = loginState.isDataValid
if (loginState.emailError != null) {
email.error = getString(loginState.emailError)
}
})
loginResult.observe(this@LoginActivity, Observer {
val loginResult = it ?: return@Observer
loading.visibility = View.GONE
if (loginResult.error != null) {
showLoginFailed(loginResult.error)
}
if (loginResult.success != null) {
sessionManager.saveAuthToken(loginResult.success)
AutoLogoutWorker.start(this@LoginActivity)
showLoginSuccess()
sendToMainActivity()
}
})
}
email.afterTextChanged {
loginViewModel.loginDataChanged(
email.text.toString(),
password.text.toString()
)
}
password.apply {
afterTextChanged {
loginViewModel.loginDataChanged(
email.text.toString(),
password.text.toString()
)
}
setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE -> {
if (!isInternetConnected) {
sendToNoInternetActivity()
} else {
loginViewModel.login(
email.text.toString(),
password.text.toString()
)
}
}
}
false
}
login.setOnClickListener {
if (!isInternetConnected) {
sendToNoInternetActivity()
return@setOnClickListener
} else {
loading.visibility = View.VISIBLE
loginViewModel.login(email.text.toString(), password.text.toString())
}
}
}
}
private fun sendToNoInternetActivity() {
val intent = Intent(this@LoginActivity, NoInternetActivity::class.java)
startActivity(intent)
}
private fun sendToMainActivity() {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intent)
finish()
}
private fun showLoginSuccess() {
Toast.makeText(applicationContext, R.string.login_success, Toast.LENGTH_LONG).show()
}
private fun showLoginFailed(error: String) {
Toast.makeText(applicationContext, error, Toast.LENGTH_SHORT).show()
}
}
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
\ No newline at end of file
package com.onionsquad.bondoman.ui.login
data class LoginFormState(
val emailError: Int? = null,
val isDataValid: Boolean = false
)
\ No newline at end of file
package com.onionsquad.bondoman.ui.login
import com.onionsquad.bondoman.auth.Token
data class LoginResult(
val success: Token? = null,
val error: String? = null
)
\ No newline at end of file
package com.onionsquad.bondoman.ui.login
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import android.util.Patterns
import com.google.gson.Gson
import com.onionsquad.bondoman.R
import com.onionsquad.bondoman.auth.SessionManager
import com.onionsquad.bondoman.auth.Token
import com.onionsquad.bondoman.network.BondomanApi
import com.onionsquad.bondoman.network.LoginRequest
import com.onionsquad.bondoman.network.LoginResponse
import com.onionsquad.bondoman.network.TokenResponse
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class LoginViewModel : ViewModel() {
private val _loginForm = MutableLiveData<LoginFormState>()
val loginFormState: LiveData<LoginFormState> = _loginForm
private val _loginResult = MutableLiveData<LoginResult>()
val loginResult: LiveData<LoginResult> = _loginResult
fun login(email: String, password: String) {
BondomanApi.retrofitService.login(LoginRequest(email, password)).enqueue(
object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
if (response.isSuccessful) {
val body = response.body()
val tokenString = Gson().fromJson(body, LoginResponse::class.java).token!!
Log.d("TOKEN", tokenString)
fetchTokenInformation(tokenString)
} else {
val errBody = response.errorBody()!!.string()
val error = when {
errBody.contains("email") -> "Invalid email"
errBody.contains("password") -> "Invalid password"
else -> "Invalid email or password"
}
_loginResult.value = LoginResult(error = error)
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
_loginResult.value = LoginResult(error = t.message)
}
}
)
}
private fun fetchTokenInformation(tokenString: String) {
BondomanApi.retrofitService.token("Bearer $tokenString").enqueue(
object : Callback<TokenResponse> {
override fun onResponse(
call: Call<TokenResponse>,
response: Response<TokenResponse>
) {
val token = response.body()!!
_loginResult.value = LoginResult(
success = Token(tokenString, token.nim!!, token.exp!!, token.iat!!)
)
}
override fun onFailure(call: Call<TokenResponse>, t: Throwable) {
_loginResult.value = LoginResult(error = t.message)
}
}
)
}
fun loginDataChanged(email: String, password: String) {
if (!isEmailValid(email)) {
_loginForm.value = LoginFormState(emailError = R.string.invalid_email)
} else {
_loginForm.value = LoginFormState(isDataValid = true)
}
}
private fun isEmailValid(username: String): Boolean {
return Patterns.EMAIL_ADDRESS.matcher(username).matches()
}
}
\ No newline at end of file
package com.onionsquad.bondoman.ui.settings
import android.app.AlertDialog
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.onionsquad.bondoman.InternetActivity
import com.onionsquad.bondoman.NoInternetActivity
import com.onionsquad.bondoman.R
import com.onionsquad.bondoman.auth.AutoLogoutWorker
import com.onionsquad.bondoman.auth.SessionManager
import com.onionsquad.bondoman.databinding.FragmentSettingsBinding
import com.onionsquad.bondoman.repository.TransactionRepository
import com.onionsquad.bondoman.room.TransactionCategory
import com.onionsquad.bondoman.room.TransactionDatabase
import com.onionsquad.bondoman.room.TransactionEntity
import io.github.evanrupert.excelkt.workbook
import org.apache.poi.ss.usermodel.FillPatternType
import org.apache.poi.ss.usermodel.IndexedColors
import java.io.File
import java.io.OutputStream
import java.time.ZoneId
import java.time.format.DateTimeFormatter
class SettingsFragment : Fragment() {
private var _binding: FragmentSettingsBinding? = null
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
private val database by lazy { TransactionDatabase.getInstance(requireContext()) }
private val repository by lazy { TransactionRepository(database.transactionDao()) }
private val saveXls =
registerForActivityResult(CreateDocument("application/vnd.ms-excel")) { uri ->
if (uri != null) {
repository.listTransactions.observe(viewLifecycleOwner) { list ->
Log.d(this::class.java.simpleName, "Saving transactions to $uri")
requireContext().contentResolver.openOutputStream(uri)?.use { fos ->
createExcelFile(list, fos)
}
}
}
}
private val saveXlsx =
registerForActivityResult(
CreateDocument("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
) { uri ->
if (uri != null) {
repository.listTransactions.observe(viewLifecycleOwner) { list ->
Log.d(this::class.java.simpleName, "Saving transactions to $uri")
requireContext().contentResolver.openOutputStream(uri)?.use { fos ->
createExcelFile(list, fos)
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val transactionViewModel =
ViewModelProvider(this).get(SettingsViewModel::class.java)
_binding = FragmentSettingsBinding.inflate(inflater, container, false)
val sessionManager = SessionManager(requireContext())
binding.apply {
buttonLogout.setOnClickListener {
val alertBuilder = AlertDialog.Builder(requireContext())
alertBuilder.setTitle(R.string.title_alert_logout)
alertBuilder.setMessage(R.string.message_alert_logout)
alertBuilder.setPositiveButton(R.string.yes) { _, _ ->
AutoLogoutWorker.stop(requireContext())
logout()
}
alertBuilder.setNegativeButton(R.string.no) { dialog, _ ->
dialog.cancel()
}
alertBuilder.show()
}
ArrayAdapter.createFromResource(
requireContext(),
R.array.excel_types,
android.R.layout.simple_spinner_dropdown_item
).also { arrayAdapter ->
arrayAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
spinnerExcelType.adapter = arrayAdapter
}
buttonSave.setOnClickListener {
when (spinnerExcelType.selectedItem.toString()) {
"XLS" -> saveXls.launch("transactions.xls")
"XLSX" -> saveXlsx.launch("transactions.xlsx")
}
Toast.makeText(requireContext(), R.string.transactions_saved, Toast.LENGTH_SHORT)
.show()
}
buttonSendEmail.setOnClickListener {
if (!(requireActivity() as InternetActivity).isInternetConnected) {
sendToNoInternetActivity()
return@setOnClickListener
}
sessionManager.ensureAuthenticated()
val token = sessionManager.fetchAuthToken()!!
val intent = Intent(Intent.ACTION_SEND).apply {
type = "text/html"
putExtra(Intent.EXTRA_EMAIL, arrayOf("${token.nim}@std.stei.itb.ac.id"))
putExtra(Intent.EXTRA_SUBJECT, "Daftar Transaksi Aplikasi Bondoman")
putExtra(
Intent.EXTRA_TEXT,
"Halo ${token.nim}, berikut daftar transaksi yang telah kamu catat dalam aplikasi Bondoman"
)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val tempFile = File(requireContext().cacheDir, "transactions.xlsx")
if (tempFile.exists()) {
tempFile.delete()
} else {
tempFile.parentFile?.mkdirs()
}
val fileUri = FileProvider.getUriForFile(
requireContext(),
"${requireContext().packageName}.fileprovider",
tempFile
)
repository.listTransactions.observe(viewLifecycleOwner) { list ->
requireContext().contentResolver.openOutputStream(fileUri)?.use { fos ->
createExcelFile(list, fos)
}
}
putExtra(Intent.EXTRA_STREAM, fileUri)
}
startActivity(Intent.createChooser(intent, "Send email"))
Toast.makeText(requireContext(), R.string.transactions_sent, Toast.LENGTH_SHORT)
.show()
}
}
return binding.root
}
private fun sendToNoInternetActivity() {
val intent = Intent(requireContext(), NoInternetActivity::class.java)
startActivity(intent)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun logout() {
val sessionManager = SessionManager(requireContext())
sessionManager.deleteAuthToken()
Toast.makeText(requireContext(), R.string.log_out_success, Toast.LENGTH_SHORT).show()
findNavController().popBackStack(R.id.navigation_transaction, true)
requireActivity().recreate()
}
private fun createExcelFile(data: List<TransactionEntity>, outputStream: OutputStream) {
workbook {
val headers = arrayOf(
"Tanggal",
"Kategori Transaksi",
"Nominal Transaksi",
"Nama Transaksi",
"Lokasi"
)
val sheet = sheet {
val headingStyle = createCellStyle {
val font = createFont { bold = true }
setFont(font)
fillPattern = FillPatternType.SOLID_FOREGROUND
fillForegroundColor = IndexedColors.AQUA.index
}
row(headingStyle) {
headers.forEach { cell(it) }
}
for (transaction in data) {
row {
val date = transaction.date
.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
.format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm:ss"))
cell(date)
cell(transaction.category.text())
cell(transaction.amount)
cell(transaction.title)
cell(transaction.location)
}
}
}.xssfSheet
for (i in 0..headers.size) {
sheet.setColumnWidth(i, 30 * 256)
}
}.xssfWorkbook.write(outputStream)
}
private fun TransactionCategory.text(): String {
return when (this) {
TransactionCategory.INCOME -> "Pemasukan"
TransactionCategory.OUTCOME -> "Pengeluaran"
TransactionCategory.UNKNOWN -> ""
}
}
}
\ No newline at end of file
package com.onionsquad.bondoman.ui.settings
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
class SettingsViewModel : ViewModel() {
private val _text = MutableLiveData<String>().apply {
value = "This is notifications Fragment"
}
val text: LiveData<String> = _text
}
\ No newline at end of file
......@@ -25,6 +25,10 @@ object Converters {
@TypeConverter
fun toTransactionCategory(categoryString: String): TransactionCategory {
return TransactionCategory.valueOf(categoryString)
return try {
TransactionCategory.valueOf(categoryString)
} catch (e: IllegalArgumentException) {
TransactionCategory.UNKNOWN
}
}
}
\ No newline at end of file