Use string identifiers instead of ints

This commit is contained in:
William Brawner 2021-01-26 21:43:33 -07:00
parent a56406caa9
commit c0ca8c8b29
34 changed files with 217 additions and 179 deletions

View file

@ -57,7 +57,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core:1.3.2'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.media:media:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.android.material:material:1.2.1'

View file

@ -4,12 +4,15 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.wbrawner.budget.*
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.AsyncViewModel
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.launch
import com.wbrawner.budget.lib.network.BaseUrlHelper
import com.wbrawner.budget.lib.network.DEFAULT_URL
import com.wbrawner.budget.lib.network.PREF_KEY_BASE_URL
import com.wbrawner.budget.postValue
import javax.inject.Inject
class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
@ -28,7 +31,7 @@ class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
suspend fun checkForExistingCredentials() {
if (baseUrlHelper.url == DEFAULT_URL) {
state.postValue(AuthenticationState.Unauthenticated)
state.postValue(AuthenticationState.Unauthenticated())
return
}
state.postValue(AsyncState.Success(AuthenticationState.Splash))
@ -36,7 +39,7 @@ class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
userRepository.getProfile()
AuthenticationState.Authenticated
} catch (ignored: Exception) {
AuthenticationState.Unauthenticated
AuthenticationState.Unauthenticated()
}
state.postValue(authState)
}
@ -53,9 +56,9 @@ class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
putString(PREF_KEY_BASE_URL, correctServer)
}
AuthenticationState.Authenticated
} catch (ignored: Exception) {
} catch (e: Exception) {
// TODO: Return error message here
AuthenticationState.Unauthenticated
AuthenticationState.Unauthenticated(server, username, password, e.message)
}
}
@ -66,6 +69,12 @@ class SplashViewModel : ViewModel(), AsyncViewModel<AuthenticationState> {
sealed class AuthenticationState {
object Splash : AuthenticationState()
object Unauthenticated : AuthenticationState()
class Unauthenticated(
val server: String? = null,
val username: String? = null,
val password: String? = null,
val errorMessage: String? = null
) : AuthenticationState()
object Authenticated : AuthenticationState()
}

View file

@ -1,15 +1,18 @@
package com.wbrawner.budget.ui.auth
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.wbrawner.budget.AsyncState
import com.wbrawner.budget.R
import com.wbrawner.budget.ui.AuthenticationState
import com.wbrawner.budget.ui.SplashViewModel
import com.wbrawner.budget.ui.ensureNotEmpty
import com.wbrawner.budget.ui.show
@ -40,6 +43,20 @@ class LoginFragment : Fragment() {
password.error = "Invalid username/password"
state.exception.printStackTrace()
}
is AsyncState.Success -> {
if (state.data is AuthenticationState.Unauthenticated) {
server.setText(state.data.server ?: "")
username.setText(state.data.username ?: "")
password.setText(state.data.password ?: "")
state.data.errorMessage?.let {
AlertDialog.Builder(view.context)
.setTitle("Login Failed")
.setMessage(it)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> }
.show()
}
}
}
}
})
password.setOnEditorActionListener { _, _, _ ->

View file

@ -19,7 +19,7 @@ import kotlinx.android.synthetic.main.fragment_add_edit_budget.*
class AddEditBudgetFragment : Fragment() {
private val viewModel: BudgetFormViewModel by viewModels()
var id: Long? = null
var id: String? = null
var menu: Menu? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -32,7 +32,7 @@ class AddEditBudgetFragment : Fragment() {
.appComponent
.inject(viewModel)
super.onAttach(context)
viewModel.getBudget(arguments?.getLong(EXTRA_BUDGET_ID))
viewModel.getBudget(arguments?.getString(EXTRA_BUDGET_ID))
}
override fun onCreateView(

View file

@ -9,8 +9,8 @@ import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.common.util.randomId
import kotlinx.coroutines.launch
import java.lang.Exception
import javax.inject.Inject
class BudgetFormViewModel : ViewModel() {
@ -25,13 +25,13 @@ class BudgetFormViewModel : ViewModel() {
@Inject
lateinit var userRepository: UserRepository
fun getBudget(id: Long? = null) {
fun getBudget(id: String? = null) {
viewModelScope.launch {
state.postValue(BudgetFormState.Loading)
try {
val budget = id?.let {
budgetRepository.findById(it)
}?: Budget(name = "")
} ?: Budget(name = "")
state.postValue(BudgetFormState.Success(budget))
} catch (e: Exception) {
state.postValue(BudgetFormState.Failed(e))
@ -46,7 +46,7 @@ class BudgetFormViewModel : ViewModel() {
if (budget.id != null) {
budgetRepository.update(budget)
} else {
budgetRepository.create(budget)
budgetRepository.create(budget.copy(id = randomId()))
}
state.postValue(BudgetFormState.Exit)
} catch (e: Exception) {
@ -55,11 +55,11 @@ class BudgetFormViewModel : ViewModel() {
}
}
fun deleteBudget(accountId: Long) {
fun deleteBudget(budgetId: String) {
viewModelScope.launch {
state.postValue(BudgetFormState.Loading)
try {
budgetRepository.delete(accountId)
budgetRepository.delete(budgetId)
state.postValue(BudgetFormState.Exit)
} catch (e: Exception) {
state.postValue(BudgetFormState.Failed(e))

View file

@ -58,7 +58,7 @@ class BudgetViewHolder(itemView: View, val navController: NavController) : Binda
}
itemView.setOnClickListener {
val bundle = Bundle().apply {
putLong(EXTRA_BUDGET_ID, budget.id!!)
putString(EXTRA_BUDGET_ID, budget.id)
}
navController.navigate(R.id.categoryListFragment, bundle)
}

View file

@ -81,8 +81,8 @@ class CategoryDetailsFragment : Fragment() {
} ?: run {
val transactionsFragment = TransactionListFragment().apply {
arguments = Bundle().apply {
putLong(EXTRA_BUDGET_ID, category.budgetId)
putLong(EXTRA_CATEGORY_ID, category.id!!)
putString(EXTRA_BUDGET_ID, category.budgetId)
putString(EXTRA_CATEGORY_ID, category.id)
}
}
childFragmentManager.beginTransaction()
@ -100,7 +100,7 @@ class CategoryDetailsFragment : Fragment() {
}
}
})
viewModel.getCategory(arguments?.getLong(EXTRA_CATEGORY_ID))
viewModel.getCategory(arguments?.getString(EXTRA_CATEGORY_ID))
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View file

@ -15,7 +15,7 @@ class CategoryDetailsViewModel : ViewModel(), AsyncViewModel<CategoryDetails> {
@Inject
lateinit var categoryRepo: CategoryRepository
fun getCategory(id: Long? = null) {
fun getCategory(id: String? = null) {
if (id == null) {
state.postValue(AsyncState.Error("Invalid category ID"))
return
@ -23,7 +23,7 @@ class CategoryDetailsViewModel : ViewModel(), AsyncViewModel<CategoryDetails> {
launch {
val category = categoryRepo.findById(id)
val multiplier = if (category.expense) -1 else 1
val balance = categoryRepo.getBalance(category.id!!) * multiplier
val balance = categoryRepo.getBalance(id) * multiplier
CategoryDetails(
category,
balance,

View file

@ -23,7 +23,7 @@ import kotlinx.android.synthetic.main.activity_add_edit_category.*
class CategoryFormActivity : AppCompatActivity() {
val viewModel: CategoryFormViewModel by viewModels()
var id: Long? = null
var id: String? = null
var menu: Menu? = null
override fun onCreate(savedInstanceState: Bundle?) {
@ -65,7 +65,7 @@ class CategoryFormActivity : AppCompatActivity() {
is AsyncState.Exit -> finish()
}
})
viewModel.loadCategory(intent?.extras?.getLong(EXTRA_CATEGORY_ID))
viewModel.loadCategory(intent?.extras?.getString(EXTRA_CATEGORY_ID))
}
override fun onCreateOptionsMenu(menu: Menu?): Boolean {

View file

@ -11,24 +11,24 @@ import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.common.util.randomId
import com.wbrawner.budget.launch
import kotlinx.coroutines.launch
import javax.inject.Inject
class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
override val state: MutableLiveData<AsyncState<CategoryFormState>> = MutableLiveData(AsyncState.Loading)
@Inject
lateinit var categoryRepository: CategoryRepository
@Inject
lateinit var budgetRepository: BudgetRepository
fun loadCategory(categoryId: Long? = null) {
fun loadCategory(categoryId: String? = null) {
launch {
val category = categoryId?.let {
categoryRepository.findById(it)
} ?: Category(-1, title = "", amount = 0)
} ?: Category("", title = "", amount = 0)
CategoryFormState(
category,
budgetRepository.findAll().toList()
@ -41,7 +41,7 @@ class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
state.postValue(AsyncState.Loading)
try {
if (category.id == null)
categoryRepository.create(category)
categoryRepository.create(category.copy(id = randomId()))
else
categoryRepository.update(category)
state.postValue(AsyncState.Exit)
@ -51,7 +51,7 @@ class CategoryFormViewModel : ViewModel(), AsyncViewModel<CategoryFormState> {
}
}
fun deleteCategoryById(id: Long) {
fun deleteCategoryById(id: String) {
viewModelScope.launch {
state.postValue(AsyncState.Loading)
try {

View file

@ -82,7 +82,7 @@ class CategoryViewHolder(
}
itemView.setOnClickListener {
val bundle = Bundle().apply {
putLong(EXTRA_CATEGORY_ID, category.id ?: -1)
putString(EXTRA_CATEGORY_ID, category.id)
putString(EXTRA_CATEGORY_NAME, category.title)
}
navController.navigate(R.id.categoryFragment, bundle)

View file

@ -34,7 +34,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
override val coroutineContext: CoroutineContext = Dispatchers.Main
private val viewModel: TransactionFormViewModel by viewModels()
var id: Long? = null
var id: String? = null
var menu: Menu? = null
var transaction: Transaction? = null
@ -112,7 +112,7 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
private suspend fun loadTransaction() {
transaction = try {
viewModel.getTransaction(intent!!.extras!!.getLong(EXTRA_TRANSACTION_ID))
viewModel.getTransaction(intent!!.extras!!.getString(EXTRA_TRANSACTION_ID)!!)
} catch (e: Exception) {
menu?.findItem(R.id.action_delete)?.isVisible = false
val date = Date()
@ -148,8 +148,8 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
this@TransactionFormActivity,
android.R.layout.simple_list_item_1
)
adapter.add(Category(id = 0, title = getString(R.string.uncategorized),
amount = 0, budgetId = 0))
adapter.add(Category(title = getString(R.string.uncategorized),
amount = 0, budgetId = ""))
adapter.addAll(categories)
edit_transaction_category.adapter = adapter
transaction?.categoryId?.let {
@ -190,10 +190,6 @@ class TransactionFormActivity : AppCompatActivity(), CoroutineScope {
}
}
val categoryId = (edit_transaction_category.selectedItem as? Category)?.id
?.let {
if (it > 0) it
else null
}
launch {
viewModel.saveTransaction(Transaction(
id = id,

View file

@ -21,7 +21,7 @@ class TransactionFormViewModel : ViewModel() {
@Inject
lateinit var userRepository: UserRepository
var currentUserId: Long? = null
var currentUserId: String? = null
private set
//TODO: Find a better way to handle this
@ -31,18 +31,18 @@ class TransactionFormViewModel : ViewModel() {
}
}
suspend fun getCategories(budgetId: Long, expense: Boolean) = categoryRepository.findAll(arrayOf(budgetId)).filter {
suspend fun getCategories(budgetId: String, expense: Boolean) = categoryRepository.findAll(arrayOf(budgetId)).filter {
it.expense == expense
}
suspend fun getTransaction(id: Long) = transactionRepository.findById(id)
suspend fun getTransaction(id: String) = transactionRepository.findById(id)
suspend fun saveTransaction(transaction: Transaction) = if (transaction.id == null)
transactionRepository.create(transaction)
else
transactionRepository.update(transaction)
suspend fun deleteTransaction(id: Long) = transactionRepository.delete(id)
suspend fun deleteTransaction(id: String) = transactionRepository.delete(id)
suspend fun getAccounts() = budgetRepository.findAll()
}

View file

@ -32,7 +32,7 @@ class TransactionListFragment : ListWithAddButtonFragment<Transaction, Transacti
get() = mapOf(TRANSACTION_VIEW to { v -> TransactionViewHolder(v, findNavController()) })
override fun reloadItems() {
viewModel.getTransactions(categoryId = arguments?.getLong(EXTRA_CATEGORY_ID))
viewModel.getTransactions(categoryId = arguments?.getString(EXTRA_CATEGORY_ID))
}
override fun bindData(data: Transaction): BindableData<Transaction> = BindableData(data, TRANSACTION_VIEW)
@ -96,8 +96,8 @@ class TransactionViewHolder(itemView: View, val navController: NavController) :
amount.setTextColor(ContextCompat.getColor(context, color))
itemView.setOnClickListener {
val bundle = Bundle().apply {
putLong(EXTRA_BUDGET_ID, transaction.budgetId)
putLong(EXTRA_TRANSACTION_ID, transaction.id ?: -1)
putString(EXTRA_BUDGET_ID, transaction.budgetId)
putString(EXTRA_TRANSACTION_ID, transaction.id)
}
navController.navigate(R.id.addEditTransactionActivity, bundle)
}

View file

@ -20,7 +20,7 @@ class TransactionListViewModel : ViewModel(), AsyncViewModel<List<Transaction>>
override val state: MutableLiveData<AsyncState<List<Transaction>>> = MutableLiveData(AsyncState.Loading)
fun getTransactions(
categoryId: Long? = null,
categoryId: String? = null,
start: Calendar? = null,
end: Calendar? = null
) {

View file

@ -1,17 +0,0 @@
package com.wbrawner.budget.lib.network
import okhttp3.Interceptor
import okhttp3.Response
const val DEFAULT_URL = "http://localhost"
class BaseUrlHelper(var url: String = DEFAULT_URL) {
val interceptor = object: Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = request.newBuilder()
val url = request.url.toString().replace(DEFAULT_URL, url)
return chain.proceed(newRequest.url(url).build())
}
}
}

View file

@ -0,0 +1,34 @@
package com.wbrawner.budget.lib.network
import android.content.SharedPreferences
import com.wbrawner.budget.common.PREF_KEY_TOKEN
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
const val DEFAULT_URL = "http://localhost"
class BaseUrlHelper(var url: String = DEFAULT_URL) {
val interceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val newRequest = request.newBuilder()
val url = request.url.toString().replace(DEFAULT_URL, url)
return chain.proceed(newRequest.url(url).build())
}
}
}
class AuthHelper @Inject constructor(val sharedPreferences: SharedPreferences) {
val interceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val token = sharedPreferences.getString(PREF_KEY_TOKEN, null)
?: return chain.proceed(chain.request())
val newRequest = chain.request()
.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain.proceed(newRequest)
}
}
}

View file

@ -1,7 +1,6 @@
package com.wbrawner.budget.lib.network
import android.content.SharedPreferences
import android.util.Base64
import com.squareup.moshi.Moshi
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter
import com.wbrawner.budget.common.budget.BudgetRepository
@ -15,16 +14,11 @@ import com.wbrawner.budget.lib.repository.NetworkUserRepository
import com.wbrawner.budgetlib.BuildConfig
import dagger.Module
import dagger.Provides
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.nio.charset.Charset
import java.util.*
import javax.inject.Named
import javax.inject.Singleton
const val PREF_KEY_BASE_URL = "baseUrl"
@ -36,36 +30,6 @@ class NetworkModule {
.add(Date::class.java, Rfc3339DateJsonAdapter())
.build()
@Provides
fun provideCookieJar(sharedPreferences: SharedPreferences): CookieJar {
return object : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
sharedPreferences.edit()
.putString(
url.host,
cookies.joinToString(separator = ",") {
Base64.encode(it.toString().toByteArray(), 0)
.toString(charset = Charset.forName("UTF-8"))
}
)
.apply()
}
override fun loadForRequest(url: HttpUrl): MutableList<Cookie> {
return sharedPreferences.getString(url.host, "")
?.split(",")
?.mapNotNull {
Cookie.parse(
url,
Base64.decode(it, 0).toString(Charset.forName("UTF-8"))
)
}
?.toMutableList()
?: mutableListOf()
}
}
}
@Singleton
@Provides
fun provideBaseUrlHelper(sharedPreferences: SharedPreferences) = BaseUrlHelper().apply {
@ -76,9 +40,9 @@ class NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(baseUrlHelper: BaseUrlHelper, cookieJar: CookieJar): OkHttpClient = OkHttpClient.Builder()
.cookieJar(cookieJar)
fun provideOkHttpClient(baseUrlHelper: BaseUrlHelper, authHelper: AuthHelper): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(baseUrlHelper.interceptor)
.addInterceptor(authHelper.interceptor)
.apply {
if (BuildConfig.DEBUG)
this.addInterceptor(
@ -100,26 +64,26 @@ class NetworkModule {
@Singleton
@Provides
fun provideApiService(retrofit: Retrofit): BudgetApiService =
retrofit.create(BudgetApiService::class.java)
fun provideApiService(retrofit: Retrofit): TwigsApiService =
retrofit.create(TwigsApiService::class.java)
@Singleton
@Provides
fun provideBudgetRepository(apiService: BudgetApiService, sharedPreferences: SharedPreferences): BudgetRepository =
fun provideBudgetRepository(apiService: TwigsApiService, sharedPreferences: SharedPreferences): BudgetRepository =
NetworkBudgetRepository(apiService, sharedPreferences)
@Singleton
@Provides
fun provideCategoryRepository(apiService: BudgetApiService): CategoryRepository =
fun provideCategoryRepository(apiService: TwigsApiService): CategoryRepository =
NetworkCategoryRepository(apiService)
@Singleton
@Provides
fun provideTransactionRepository(apiService: BudgetApiService): TransactionRepository =
fun provideTransactionRepository(apiService: TwigsApiService): TransactionRepository =
NetworkTransactionRepository(apiService)
@Singleton
@Provides
fun provideUserRepository(apiService: BudgetApiService): UserRepository =
NetworkUserRepository(apiService)
fun provideUserRepository(apiService: TwigsApiService, sharedPreferences: SharedPreferences): UserRepository =
NetworkUserRepository(apiService, sharedPreferences)
}

View file

@ -1,5 +1,6 @@
package com.wbrawner.budget.lib.network
import com.wbrawner.budget.common.Session
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.transaction.Transaction
@ -8,7 +9,7 @@ import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.lib.repository.NewBudgetRequest
import retrofit2.http.*
interface BudgetApiService {
interface TwigsApiService {
// Budgets
@GET("budgets")
suspend fun getBudgets(
@ -17,54 +18,54 @@ interface BudgetApiService {
): List<Budget>
@GET("budgets/{id}")
suspend fun getBudget(@Path("id") id: Long): Budget
suspend fun getBudget(@Path("id") id: String): Budget
@GET("budgets/{id}/balance")
suspend fun getBudgetBalance(@Path("id") id: Long): BudgetBalanceResponse
suspend fun getBudgetBalance(@Path("id") id: String): BudgetBalanceResponse
@POST("budgets")
suspend fun newBudget(@Body budget: NewBudgetRequest): Budget
@PUT("budgets/{id}")
suspend fun updateBudget(
@Path("id") id: Long,
@Path("id") id: String,
@Body budget: Budget
): Budget
@DELETE("budgets/{id}")
suspend fun deleteBudget(@Path("id") id: Long)
suspend fun deleteBudget(@Path("id") id: String)
// Categories
@GET("categories")
suspend fun getCategories(
@Query("budgetIds") budgetIds: Array<Long>? = null,
@Query("budgetIds") budgetIds: Array<String>? = null,
@Query("count") count: Int? = null,
@Query("page") page: Int? = null
): List<Category>
@GET("categories/{id}")
suspend fun getCategory(@Path("id") id: Long): Category
suspend fun getCategory(@Path("id") id: String): Category
@GET("categories/{id}/balance")
suspend fun getCategoryBalance(@Path("id") id: Long): CategoryBalanceResponse
suspend fun getCategoryBalance(@Path("id") id: String): CategoryBalanceResponse
@POST("categories")
suspend fun newCategory(@Body category: Category): Category
@PUT("categories/{id}")
suspend fun updateCategory(
@Path("id") id: Long,
@Path("id") id: String,
@Body category: Category
): Category
@DELETE("categories/{id}")
suspend fun deleteCategory(@Path("id") id: Long)
suspend fun deleteCategory(@Path("id") id: String)
// Transactions
@GET("transactions")
suspend fun getTransactions(
@Query("budgetIds") budgetIds: List<Long>? = null,
@Query("categoryIds") categoryIds: List<Long>? = null,
@Query("budgetIds") budgetIds: List<String>? = null,
@Query("categoryIds") categoryIds: List<String>? = null,
@Query("from") from: String? = null,
@Query("to") to: String? = null,
@Query("count") count: Int? = null,
@ -72,30 +73,30 @@ interface BudgetApiService {
): List<Transaction>
@GET("transactions/{id}")
suspend fun getTransaction(@Path("id") id: Long): Transaction
suspend fun getTransaction(@Path("id") id: String): Transaction
@POST("transactions")
suspend fun newTransaction(@Body transaction: Transaction): Transaction
@PUT("transactions/{id}")
suspend fun updateTransaction(
@Path("id") id: Long,
@Path("id") id: String,
@Body transaction: Transaction
): Transaction
@DELETE("transactions/{id}")
suspend fun deleteTransaction(@Path("id") id: Long)
suspend fun deleteTransaction(@Path("id") id: String)
// Users
@GET("users")
suspend fun getUsers(
@Query("budgetId") budgetId: Long? = null,
@Query("budgetId") budgetid: String? = null,
@Query("count") count: Int? = null,
@Query("page") page: Int? = null
): List<User>
@POST("users/login")
suspend fun login(@Body request: LoginRequest): User
suspend fun login(@Body request: LoginRequest): Session
@GET("users/me")
suspend fun getProfile(): User
@ -104,17 +105,17 @@ interface BudgetApiService {
suspend fun searchUsers(@Query("query") query: String): List<User>
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Long): User
suspend fun getUser(@Path("id") id: String): User
@POST("users")
suspend fun newUser(@Body user: User): User
@PUT("users/{id}")
suspend fun updateUser(
@Path("id") id: Long,
@Path("id") id: String,
@Body user: User
): User
@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") id: Long)
suspend fun deleteUser(@Path("id") id: String)
}

View file

@ -5,7 +5,7 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.wbrawner.budget.common.budget.Budget
import com.wbrawner.budget.common.budget.BudgetRepository
import com.wbrawner.budget.lib.network.BudgetApiService
import com.wbrawner.budget.lib.network.TwigsApiService
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
@ -14,7 +14,7 @@ import kotlinx.coroutines.sync.withLock
const val KEY_DEFAULT_BUDGET = "defaultBudget"
class NetworkBudgetRepository(
private val apiService: BudgetApiService,
private val apiService: TwigsApiService,
private val sharedPreferences: SharedPreferences
) : BudgetRepository {
private val mutex = Mutex()
@ -25,7 +25,7 @@ class NetworkBudgetRepository(
currentBudget.observeForever { budget ->
sharedPreferences.edit().apply {
budget?.id?.let {
putLong(KEY_DEFAULT_BUDGET, it)
putString(KEY_DEFAULT_BUDGET, it)
} ?: remove(KEY_DEFAULT_BUDGET)
apply()
}
@ -42,7 +42,7 @@ class NetworkBudgetRepository(
emptyList<Budget>()
}
if (budgets.isEmpty()) return
val budgetId = sharedPreferences.getLong(KEY_DEFAULT_BUDGET, budgets.first().id!!)
val budgetId = sharedPreferences.getString(KEY_DEFAULT_BUDGET, budgets.first().id)!!
val budget = try {
findById(budgetId)
} catch (e: Exception) {
@ -67,7 +67,7 @@ class NetworkBudgetRepository(
}
}
override suspend fun findById(id: Long, setCurrent: Boolean): Budget = (mutex.withLock {
override suspend fun findById(id: String, setCurrent: Boolean): Budget = (mutex.withLock {
budgets.firstOrNull { it.id == id }
} ?: apiService.getBudget(id).apply {
mutex.withLock {
@ -87,19 +87,19 @@ class NetworkBudgetRepository(
}
}
override suspend fun delete(id: Long) = apiService.deleteBudget(id).apply {
override suspend fun delete(id: String) = apiService.deleteBudget(id).apply {
mutex.withLock {
budgets.removeAll { it.id == id }
}
}
override suspend fun getBalance(id: Long): Long = apiService.getBudgetBalance(id).balance
override suspend fun getBalance(id: String): Long = apiService.getBudgetBalance(id).balance
}
data class NewBudgetRequest(
val name: String,
val description: String? = null,
val userIds: List<Long>
val userIds: List<String>
) {
constructor(budget: Budget) : this(budget.name, budget.description, budget.users.map { it.id!! })
}

View file

@ -2,23 +2,24 @@ package com.wbrawner.budget.lib.repository
import com.wbrawner.budget.common.category.Category
import com.wbrawner.budget.common.category.CategoryRepository
import com.wbrawner.budget.lib.network.BudgetApiService
import com.wbrawner.budget.lib.network.TwigsApiService
import javax.inject.Inject
class NetworkCategoryRepository @Inject constructor(private val apiService: BudgetApiService) : CategoryRepository {
class NetworkCategoryRepository @Inject constructor(private val apiService: TwigsApiService) : CategoryRepository {
override suspend fun create(newItem: Category): Category = apiService.newCategory(newItem)
override suspend fun findAll(budgetIds: Array<Long>?): List<Category> = apiService.getCategories(budgetIds).sortedBy { it.title }
override suspend fun findAll(budgetIds: Array<String>?): List<Category> = apiService.getCategories(budgetIds).sortedBy { it.title }
override suspend fun findAll(): List<Category> = findAll(null)
override suspend fun findById(id: Long): Category = apiService.getCategory(id)
override suspend fun findById(id: String): Category = apiService.getCategory(id)
override suspend fun update(updatedItem: Category): Category =
apiService.updateCategory(updatedItem.id!!, updatedItem)
override suspend fun delete(id: Long) = apiService.deleteCategory(id)
override suspend fun delete(id: String) = apiService.deleteCategory(id)
// TODO: Implement this method server-side and then here
override suspend fun getBalance(id: Long): Long = apiService.getCategoryBalance(id).balance
override suspend fun getBalance(id: String): Long = apiService.getCategoryBalance(id).balance
}

View file

@ -2,19 +2,19 @@ package com.wbrawner.budget.lib.repository
import com.wbrawner.budget.common.transaction.Transaction
import com.wbrawner.budget.common.transaction.TransactionRepository
import com.wbrawner.budget.lib.network.BudgetApiService
import com.wbrawner.budget.lib.network.TwigsApiService
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
class NetworkTransactionRepository @Inject constructor(private val apiService: BudgetApiService) : TransactionRepository {
class NetworkTransactionRepository @Inject constructor(private val apiService: TwigsApiService) : TransactionRepository {
private val dateFormatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH)
override suspend fun create(newItem: Transaction): Transaction = apiService.newTransaction(newItem)
override suspend fun findAll(
budgetIds: List<Long>?,
categoryIds: List<Long>?,
budgetIds: List<String>?,
categoryIds: List<String>?,
start: Calendar?,
end: Calendar?
): List<Transaction> = apiService.getTransactions(
@ -30,10 +30,10 @@ class NetworkTransactionRepository @Inject constructor(private val apiService: B
override suspend fun findAll(): List<Transaction> = findAll(null)
override suspend fun findById(id: Long): Transaction = apiService.getTransaction(id)
override suspend fun findById(id: String): Transaction = apiService.getTransaction(id)
override suspend fun update(updatedItem: Transaction): Transaction =
apiService.updateTransaction(updatedItem.id!!, updatedItem)
override suspend fun delete(id: Long) = apiService.deleteTransaction(id)
override suspend fun delete(id: String) = apiService.deleteTransaction(id)
}

View file

@ -1,16 +1,21 @@
package com.wbrawner.budget.lib.repository
import android.content.SharedPreferences
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.wbrawner.budget.common.PREF_KEY_TOKEN
import com.wbrawner.budget.common.user.LoginRequest
import com.wbrawner.budget.common.user.User
import com.wbrawner.budget.common.user.UserRepository
import com.wbrawner.budget.lib.network.BudgetApiService
import com.wbrawner.budget.lib.network.TwigsApiService
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import javax.inject.Inject
class NetworkUserRepository @Inject constructor(private val apiService: BudgetApiService) : UserRepository {
class NetworkUserRepository @Inject constructor(
private val apiService: TwigsApiService,
private val sharedPreferences: SharedPreferences
) : UserRepository {
override val currentUser: LiveData<User?> = MutableLiveData()
@ -23,10 +28,14 @@ class NetworkUserRepository @Inject constructor(private val apiService: BudgetAp
}
}
override suspend fun login(username: String, password: String): User =
apiService.login(LoginRequest(username, password)).also {
(currentUser as MutableLiveData).postValue(it)
}
override suspend fun login(username: String, password: String): User {
apiService.login(LoginRequest(username, password)).also {
sharedPreferences.edit()
.putString(PREF_KEY_TOKEN, it.token)
.apply()
}
return getProfile()
}
override suspend fun getProfile(): User = apiService.getProfile().also {
(currentUser as MutableLiveData).postValue(it)
@ -34,11 +43,11 @@ class NetworkUserRepository @Inject constructor(private val apiService: BudgetAp
override suspend fun create(newItem: User): User = apiService.newUser(newItem)
override suspend fun findAll(accountId: Long?): List<User> = apiService.getUsers(accountId)
override suspend fun findAll(budgetId: String?): List<User> = apiService.getUsers(budgetId)
override suspend fun findAll(): List<User> = findAll(null)
override suspend fun findById(id: Long): User = apiService.getUser(id)
override suspend fun findById(id: String): User = apiService.getUser(id)
override suspend fun findAllByNameLike(query: String): List<User> =
apiService.searchUsers(query)
@ -46,5 +55,5 @@ class NetworkUserRepository @Inject constructor(private val apiService: BudgetAp
override suspend fun update(updatedItem: User): User =
apiService.updateUser(updatedItem.id!!, updatedItem)
override suspend fun delete(id: Long) = apiService.deleteUser(id)
}
override suspend fun delete(id: String) = apiService.deleteUser(id)
}

View file

@ -15,5 +15,5 @@ interface Repository<T, K> {
}
interface Identifiable {
val id: Long?
val id: String?
}

View file

@ -0,0 +1,11 @@
package com.wbrawner.budget.common
import java.util.*
const val PREF_KEY_TOKEN = "sessionToken"
data class Session(
val id: String,
val token: String,
val expiration: Date
)

View file

@ -4,7 +4,7 @@ import com.wbrawner.budget.common.Identifiable
import com.wbrawner.budget.common.user.User
data class Budget(
override val id: Long? = null,
override val id: String? = null,
val name: String,
val description: String? = null,
val users: List<User> = emptyList()

View file

@ -3,10 +3,10 @@ package com.wbrawner.budget.common.budget
import androidx.lifecycle.LiveData
import com.wbrawner.budget.common.Repository
interface BudgetRepository : Repository<Budget, Long> {
interface BudgetRepository : Repository<Budget, String> {
val currentBudget: LiveData<Budget?>
override suspend fun findById(id: Long): Budget = findById(id, false)
suspend fun findById(id: Long, setCurrent: Boolean = false): Budget
override suspend fun findById(id: String): Budget = findById(id, false)
suspend fun findById(id: String, setCurrent: Boolean = false): Budget
suspend fun prefetchData()
suspend fun getBalance(id: Long): Long
suspend fun getBalance(id: String): Long
}

View file

@ -3,8 +3,8 @@ package com.wbrawner.budget.common.category
import com.wbrawner.budget.common.Identifiable
data class Category(
val budgetId: Long,
override val id: Long? = null,
val budgetId: String,
override val id: String? = null,
val title: String,
val description: String? = null,
val amount: Long,

View file

@ -2,7 +2,7 @@ package com.wbrawner.budget.common.category
import com.wbrawner.budget.common.Repository
interface CategoryRepository : Repository<Category, Long> {
suspend fun findAll(budgetIds: Array<Long>? = null): List<Category>
suspend fun getBalance(id: Long): Long
interface CategoryRepository : Repository<Category, String> {
suspend fun findAll(budgetIds: Array<String>? = null): List<Category>
suspend fun getBalance(id: String): Long
}

View file

@ -4,13 +4,13 @@ import com.wbrawner.budget.common.Identifiable
import java.util.*
data class Transaction(
override val id: Long? = null,
override val id: String? = null,
val title: String,
val date: Date,
val description: String,
val amount: Long,
val categoryId: Long? = null,
val budgetId: Long,
val categoryId: String? = null,
val budgetId: String,
val expense: Boolean,
val createdBy: Long
val createdBy: String
): Identifiable

View file

@ -3,10 +3,10 @@ package com.wbrawner.budget.common.transaction
import com.wbrawner.budget.common.Repository
import java.util.*
interface TransactionRepository : Repository<Transaction, Long> {
interface TransactionRepository : Repository<Transaction, String> {
suspend fun findAll(
budgetIds: List<Long>? = null,
categoryIds: List<Long>? = null,
budgetIds: List<String>? = null,
categoryIds: List<String>? = null,
start: Calendar? = GregorianCalendar.getInstance().apply {
set(Calendar.DAY_OF_MONTH, 1)
set(Calendar.HOUR_OF_DAY, 0)

View file

@ -1,11 +1,13 @@
package com.wbrawner.budget.common.user
import com.wbrawner.budget.common.Identifiable
data class User(
val id: Long? = null,
override val id: String? = null,
val username: String,
val email: String? = null,
val avatar: String? = null
) {
) : Identifiable {
override fun toString(): String = username
}

View file

@ -3,10 +3,10 @@ package com.wbrawner.budget.common.user
import androidx.lifecycle.LiveData
import com.wbrawner.budget.common.Repository
interface UserRepository : Repository<User, Long> {
interface UserRepository : Repository<User, String> {
val currentUser: LiveData<User?>
suspend fun login(username: String, password: String): User
suspend fun getProfile(): User
suspend fun findAll(accountId: Long? = null): List<User>
suspend fun findAll(budgetId: String? = null): List<User>
suspend fun findAllByNameLike(query: String): List<User>
}

View file

@ -0,0 +1,11 @@
package com.wbrawner.budget.common.util
private const val CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
fun randomId(): String {
val id = StringBuilder()
repeat(32) {
id.append(CHARACTERS.random())
}
return id.toString()
}