├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── api.properties ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro ├── schemas │ └── com.arduia.expense.data.local.ProExpenseDatabase │ │ ├── 4.json │ │ ├── 5.json │ │ └── 6.json └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── arduia │ │ └── expense │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── currencies.json │ │ ├── open_source_licenses.html │ │ └── privacy_policy.html │ ├── java │ │ └── com │ │ │ └── arduia │ │ │ └── expense │ │ │ ├── ExpenseApplication.kt │ │ │ ├── data │ │ │ ├── BackupRepository.kt │ │ │ ├── BackupRepositoryImpl.kt │ │ │ ├── CurrencyRepository.kt │ │ │ ├── CurrencyRepositoryImpl.kt │ │ │ ├── ExpenseRepository.kt │ │ │ ├── ExpenseRepositoryImpl.kt │ │ │ ├── FeedbackWorker.kt │ │ │ ├── ProExpenseServerRepository.kt │ │ │ ├── ProExpenseServerRepositoryImpl.kt │ │ │ ├── SettingsRepository.kt │ │ │ ├── SettingsRepositoryImpl.kt │ │ │ ├── backup │ │ │ │ ├── ExpenseBackupSheet.kt │ │ │ │ ├── ExpenseBackupSource.kt │ │ │ │ ├── ExportWorker.kt │ │ │ │ ├── ImportWorker.kt │ │ │ │ └── SchemaBackupSheet.kt │ │ │ ├── exception │ │ │ │ └── RepositoryException.kt │ │ │ ├── ext │ │ │ │ ├── Context.kt │ │ │ │ └── Result.kt │ │ │ ├── local │ │ │ │ ├── AboutUpdateDataModel.kt │ │ │ │ ├── AmountTypeConverter.kt │ │ │ │ ├── BackupDao.kt │ │ │ │ ├── BackupEnt.kt │ │ │ │ ├── CacheDao.kt │ │ │ │ ├── CacheDaoImpl.kt │ │ │ │ ├── CurrencyDao.kt │ │ │ │ ├── CurrencyDaoImpl.kt │ │ │ │ ├── CurrencyDto.kt │ │ │ │ ├── DateRangeDataModel.kt │ │ │ │ ├── ExpenseDao.kt │ │ │ │ ├── ExpenseEnt.kt │ │ │ │ ├── PreferenceFlowStorageDaoImpl.kt │ │ │ │ ├── PreferenceStorageDao.kt │ │ │ │ ├── ProExpenseDatabase.kt │ │ │ │ └── UpdateStatusDataModel.kt │ │ │ ├── network │ │ │ │ ├── CheckUpdateDto.kt │ │ │ │ ├── ExpenseNetworkDao.kt │ │ │ │ ├── ExpenseVersionDto.kt │ │ │ │ └── FeedbackDto.kt │ │ │ └── update │ │ │ │ └── CheckAboutUpdateWorker.kt │ │ │ ├── di │ │ │ ├── AbstractBackupModule.kt │ │ │ ├── AbstractDomainModule.kt │ │ │ ├── AbstractExpenseModule.kt │ │ │ ├── AbstractFormattingModule.kt │ │ │ ├── AbstractMapperModule.kt │ │ │ ├── AbstractRepoModule.kt │ │ │ ├── AbstractUiModelMapperModule.kt │ │ │ ├── AdapterModule.kt │ │ │ ├── AnimationModule.kt │ │ │ ├── BackgroundModule.kt │ │ │ ├── BackupMessagingModule.kt │ │ │ ├── BackupModule.kt │ │ │ ├── ContentProviderModule.kt │ │ │ ├── DatabaseModule.kt │ │ │ ├── FormatModule.kt │ │ │ ├── NavHostModule.kt │ │ │ ├── NetworkModule.kt │ │ │ └── RepositoryModule.kt │ │ │ ├── domain │ │ │ ├── Amount.kt │ │ │ ├── DataStoreExchangeRate.kt │ │ │ ├── ExpenseLogItemEnt.kt │ │ │ ├── ExpenseStore.kt │ │ │ └── filter │ │ │ │ ├── DateRange.kt │ │ │ │ ├── ExpenseDateRange.kt │ │ │ │ ├── ExpenseLogFilterInfo.kt │ │ │ │ └── Range.kt │ │ │ ├── model │ │ │ ├── FlowResult.kt │ │ │ └── Result.kt │ │ │ └── ui │ │ │ ├── BackupMessageReceiver.kt │ │ │ ├── MainActivity.kt │ │ │ ├── MainHost.kt │ │ │ ├── MainViewModel.kt │ │ │ ├── NavBaseFragment.kt │ │ │ ├── NavigationDrawer.kt │ │ │ ├── about │ │ │ ├── AboutFragment.kt │ │ │ ├── AboutUpdateDialog.kt │ │ │ ├── AboutUpdateUiModel.kt │ │ │ └── AboutUpdateUiModelMapper.kt │ │ │ ├── backup │ │ │ ├── BackupFragment.kt │ │ │ ├── BackupListAdapter.kt │ │ │ ├── BackupMapper.kt │ │ │ ├── BackupMessageViewModel.kt │ │ │ ├── BackupUiModel.kt │ │ │ ├── BackupViewModel.kt │ │ │ ├── ExportDialogFragment.kt │ │ │ ├── ExportViewModel.kt │ │ │ ├── ImportDialogFragment.kt │ │ │ └── ImportViewModel.kt │ │ │ ├── common │ │ │ ├── category │ │ │ │ ├── ExpenseCategoryProvider.kt │ │ │ │ └── ExpenseCategoryProviderImpl.kt │ │ │ ├── customview │ │ │ │ └── MaterialSearchBox.kt │ │ │ ├── delete │ │ │ │ └── DeleteConfirmFragment.kt │ │ │ ├── exception │ │ │ │ └── DateRangeCheck.kt │ │ │ ├── expense │ │ │ │ ├── ExpenseDetailDialog.kt │ │ │ │ └── ExpenseDetailUiModel.kt │ │ │ ├── ext │ │ │ │ ├── Activity.kt │ │ │ │ ├── CalendarExt.kt │ │ │ │ └── ColorList.kt │ │ │ ├── filter │ │ │ │ ├── DateRangeSortingEnt.kt │ │ │ │ ├── ExpenseFilterDialogFragment.kt │ │ │ │ └── ExpenseFilterViewModel.kt │ │ │ ├── formatter │ │ │ │ ├── DateFormatter.kt │ │ │ │ ├── DateRangeFormatter.kt │ │ │ │ ├── ExpenseDateFormatter.kt │ │ │ │ ├── ExpenseDateRangeFormatter.kt │ │ │ │ ├── ExpenseRecentDateFormatter.kt │ │ │ │ ├── MonthDateRangeFormatter.kt │ │ │ │ └── StatisticDateRangeFormatter.kt │ │ │ ├── helper │ │ │ │ └── MarginItemDecoration.kt │ │ │ ├── language │ │ │ │ ├── LanguageProvider.kt │ │ │ │ ├── LanguageProviderImpl.kt │ │ │ │ └── LanguageUiModel.kt │ │ │ ├── mapper │ │ │ │ └── CurrencyUiModelMapper.kt │ │ │ └── uimodel │ │ │ │ └── DeleteInfoUiModel.kt │ │ │ ├── entry │ │ │ ├── CategoryListAdapter.kt │ │ │ ├── ExpenseEntryFragment.kt │ │ │ ├── ExpenseEntryMode.kt │ │ │ ├── ExpenseEntryViewModel.kt │ │ │ ├── ExpenseUpdateDataUiModel.kt │ │ │ ├── ExpenseUpdateDataUiModelMapper.kt │ │ │ ├── FloatingInputFilter.kt │ │ │ └── LockMode.kt │ │ │ ├── expenselogs │ │ │ ├── ExpenseEntToLogVoMapper.kt │ │ │ ├── ExpenseFragment.kt │ │ │ ├── ExpenseLogAdapter.kt │ │ │ ├── ExpenseLogUiModel.kt │ │ │ ├── ExpenseLogUiModelMapper.kt │ │ │ ├── ExpenseMode.kt │ │ │ ├── ExpenseUiModel.kt │ │ │ ├── ExpenseViewModel.kt │ │ │ └── swipe │ │ │ │ ├── SwipeFrameLayout.kt │ │ │ │ ├── SwipeItemCallback.kt │ │ │ │ ├── SwipeItemState.kt │ │ │ │ ├── SwipeListenerVH.kt │ │ │ │ └── SwipeStateHolder.kt │ │ │ ├── feedback │ │ │ ├── FeedbackFragment.kt │ │ │ ├── FeedbackStatusDialog.kt │ │ │ └── FeedbackViewModel.kt │ │ │ ├── home │ │ │ ├── ExpenseDayNameProvider.kt │ │ │ ├── ExpenseDetailUiModelMapper.kt │ │ │ ├── ExpenseGraphAdapter.kt │ │ │ ├── ExpenseRateCalculator.kt │ │ │ ├── ExpenseRateCalculatorImpl.kt │ │ │ ├── ExpenseUiModelMapper.kt │ │ │ ├── HomeEpoxyController.kt │ │ │ ├── HomeFragment.kt │ │ │ ├── HomeViewModel.kt │ │ │ ├── IncomeOutcomeEpoxyModel.kt │ │ │ ├── RecentEpoxyModel.kt │ │ │ ├── RecentListAdapter.kt │ │ │ └── WeeklyGraphUiModel.kt │ │ │ ├── onboarding │ │ │ ├── ChooseCurrencyFragment.kt │ │ │ ├── ChooseCurrencyViewModel.kt │ │ │ ├── ChooseLanguageFragment.kt │ │ │ ├── ChooseLanguageViewModel.kt │ │ │ ├── CurrencyListAdapter.kt │ │ │ ├── CurrencyUiModel.kt │ │ │ ├── LangListAdapter.kt │ │ │ ├── LanguageListAdapter.kt │ │ │ ├── OnBoardingConfigFragment.kt │ │ │ ├── OnBoardingConfigViewModel.kt │ │ │ └── OnBoardingStateAdapter.kt │ │ │ ├── settings │ │ │ ├── ChooseCurrencyDialog.kt │ │ │ ├── ChooseLanguageDialog.kt │ │ │ ├── ChooseThemeDialog.kt │ │ │ ├── SettingsFragment.kt │ │ │ └── SettingsViewModel.kt │ │ │ ├── splash │ │ │ ├── SplashFragment.kt │ │ │ └── SplashViewModel.kt │ │ │ ├── statistics │ │ │ ├── CategoryAnalysizerImpl.kt │ │ │ ├── CategoryAnalyzer.kt │ │ │ ├── CategoryStatisticListAdapter.kt │ │ │ ├── CategoryStatisticUiModel.kt │ │ │ ├── StatisticsFragment.kt │ │ │ └── StatisticsViewModel.kt │ │ │ └── web │ │ │ └── WebFragment.kt │ └── res │ │ ├── anim │ │ ├── expense_enter_left.xml │ │ ├── expense_exit_right.xml │ │ ├── pop_down_up.xml │ │ └── pop_up_down.xml │ │ ├── color │ │ ├── category_background_color_statelist.xml │ │ └── navigation_menu_text_color.xml │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bg_large_rounded_warning.xml │ │ ├── bg_small_rounded_warning.xml │ │ ├── flag_china.xml │ │ ├── flag_germany.xml │ │ ├── flag_myanmar.xml │ │ ├── flag_united_states.xml │ │ ├── ic_accomplish.xml │ │ ├── ic_add.xml │ │ ├── ic_arrow_start.xml │ │ ├── ic_back.xml │ │ ├── ic_backup.xml │ │ ├── ic_borrow.xml │ │ ├── ic_calendar.xml │ │ ├── ic_check.xml │ │ ├── ic_checked.xml │ │ ├── ic_close.xml │ │ ├── ic_clothes.xml │ │ ├── ic_date.xml │ │ ├── ic_delete.xml │ │ ├── ic_donation.xml │ │ ├── ic_done.xml │ │ ├── ic_done_circle.xml │ │ ├── ic_down_arrow.xml │ │ ├── ic_drop_down.xml │ │ ├── ic_edit.xml │ │ ├── ic_education.xml │ │ ├── ic_entertainment.xml │ │ ├── ic_expense_logs.xml │ │ ├── ic_export.xml │ │ ├── ic_feedback.xml │ │ ├── ic_filter.xml │ │ ├── ic_folder.xml │ │ ├── ic_food.xml │ │ ├── ic_healthcare.xml │ │ ├── ic_history.xml │ │ ├── ic_home.xml │ │ ├── ic_household.xml │ │ ├── ic_import.xml │ │ ├── ic_income.xml │ │ ├── ic_info.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_background_rounded.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_foreground_rounded.xml │ │ ├── ic_like.xml │ │ ├── ic_lock_closed.xml │ │ ├── ic_lock_open.xml │ │ ├── ic_loop.xml │ │ ├── ic_menu.xml │ │ ├── ic_minus.xml │ │ ├── ic_more.xml │ │ ├── ic_more_horizontal.xml │ │ ├── ic_more_next.xml │ │ ├── ic_nav_next.xml │ │ ├── ic_outcome.xml │ │ ├── ic_report.xml │ │ ├── ic_save.xml │ │ ├── ic_search.xml │ │ ├── ic_select_all.xml │ │ ├── ic_settings.xml │ │ ├── ic_social.xml │ │ ├── ic_statistics.xml │ │ ├── ic_theme.xml │ │ ├── ic_time.xml │ │ ├── ic_transportation.xml │ │ ├── ic_up_arrrow.xml │ │ └── ic_update.xml │ │ ├── font │ │ ├── poppins.xml │ │ ├── poppins_light.ttf │ │ ├── poppins_medium.ttf │ │ ├── pyidaungsu.xml │ │ ├── pyidaungsu_bold.ttf │ │ └── pyidaungsu_regular.ttf │ │ ├── layout │ │ ├── activ_main.xml │ │ ├── choose_theme_dialog.xml │ │ ├── expense_detail_dialog.xml │ │ ├── filter_expense_dialog.xml │ │ ├── frag_about.xml │ │ ├── frag_about_update_dialog.xml │ │ ├── frag_backup.xml │ │ ├── frag_backup_detail.xml │ │ ├── frag_choose_currency.xml │ │ ├── frag_choose_currency_dialog.xml │ │ ├── frag_choose_language.xml │ │ ├── frag_choose_language_dialog.xml │ │ ├── frag_delete_confirm_dialog.xml │ │ ├── frag_expense_entry.xml │ │ ├── frag_expense_logs.xml │ │ ├── frag_export_dialog.xml │ │ ├── frag_feedback.xml │ │ ├── frag_feedback_status_dialog.xml │ │ ├── frag_home.xml │ │ ├── frag_lang_dialog.xml │ │ ├── frag_onboard_config.xml │ │ ├── frag_settings.xml │ │ ├── frag_splash.xml │ │ ├── frag_statistic.xml │ │ ├── frag_web.xml │ │ ├── item_backup.xml │ │ ├── item_category.xml │ │ ├── item_category_statistic.xml │ │ ├── item_currency.xml │ │ ├── item_expense_date_header.xml │ │ ├── item_expense_log.xml │ │ ├── item_expense_recent.xml │ │ ├── item_language.xml │ │ ├── layout_expense_graph.xml │ │ ├── layout_expense_in_out.xml │ │ ├── layout_header.xml │ │ ├── layout_no_expense_logs.xml │ │ ├── layout_recent_lists.xml │ │ ├── layout_search_box.xml │ │ └── layout_toolbar.xml │ │ ├── menu │ │ ├── menu_entry.xml │ │ ├── menu_expense_log.xml │ │ └── menu_home.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── navigation │ │ └── main_nav.xml │ │ ├── values-cn │ │ └── strings.xml │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-my │ │ ├── font_config.xml │ │ └── strings.xml │ │ ├── values-night │ │ └── theme.xml │ │ ├── values-ru │ │ └── strings.xml │ │ └── values │ │ ├── attr.xml │ │ ├── base_theme.xml │ │ ├── colors.xml │ │ ├── dimen.xml │ │ ├── font_config.xml │ │ ├── integer.xml │ │ ├── strings.xml │ │ ├── styles.xml │ │ └── theme.xml │ └── test │ └── java │ └── com │ └── arduia │ └── expense │ └── ExampleUnitTest.kt ├── backup ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── arduia │ │ └── backup │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── arduia │ │ └── backup │ │ ├── AbstractBackupSheet.kt │ │ ├── BackupException.kt │ │ ├── BackupSheet.kt │ │ ├── BackupSource.kt │ │ ├── ExcelBackup.kt │ │ ├── FileNameGenerator.kt │ │ ├── NameGenerator.kt │ │ ├── SheetFieldInfo.kt │ │ ├── SheetRow.kt │ │ ├── generator │ │ └── BackupNameGenerator.kt │ │ └── task │ │ ├── BackupResult.kt │ │ └── BackupTask.kt │ └── test │ └── java │ └── com │ └── arduia │ └── backup │ └── ExampleUnitTest.kt ├── build.gradle ├── currency-store ├── .gitignore ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── arduia │ └── currencystore │ ├── Rate.kt │ └── Store.kt ├── expense-backup ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── arduia │ │ └── expense │ │ └── backup │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── arduia │ │ └── expense │ │ └── backup │ │ ├── MainCategoryField.kt │ │ ├── Metadata.kt │ │ └── schema │ │ ├── BackupSchema.kt │ │ └── table │ │ ├── Field.kt │ │ └── Table.kt │ └── test │ └── java │ └── com │ └── arduia │ └── expense │ └── backup │ └── ExampleUnitTest.kt ├── fastlane └── metadata │ └── android │ └── en-US │ ├── changelogs │ ├── 2.txt │ ├── 3.txt │ ├── 4.txt │ ├── 5.txt │ ├── 6.txt │ ├── 7.txt │ └── 8.txt │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ └── 4.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── shared ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── arduia │ │ └── core │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── arduia │ │ └── core │ │ ├── arch │ │ └── Mapper.kt │ │ ├── content │ │ └── Context.kt │ │ ├── extension │ │ ├── Dimen.kt │ │ └── Drawable.kt │ │ ├── lang │ │ └── LocaleUpdate.kt │ │ ├── performance │ │ └── Duration.kt │ │ └── view │ │ └── View.kt │ └── test │ └── java │ └── com │ └── arduia │ └── core │ └── ExampleUnitTest.kt └── week-expense-graph ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src ├── androidTest └── java │ └── com │ └── arduia │ └── graph │ └── ExampleInstrumentedTest.kt ├── main ├── AndroidManifest.xml ├── java │ └── com │ │ └── arduia │ │ └── graph │ │ ├── DayNameProvider.kt │ │ ├── DefaultDayNameProviderImpl.kt │ │ ├── SpendGraph.kt │ │ └── SpendPoint.kt └── res │ └── values │ └── attrs.xml └── test └── java └── com └── arduia └── graph └── ExampleUnitTest.kt /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | android: circleci/android@0.2.1 5 | 6 | jobs: 7 | build: 8 | executor: android/android 9 | 10 | steps: 11 | - checkout 12 | - run: 13 | name: chmod permissions 14 | command: chmod +x ./gradlew 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | /local.properties 3 | .DS_Store 4 | /build 5 | /captures 6 | .externalNativeBuild 7 | .cxx 8 | /.gradle/ 9 | /.idea/ 10 | -------------------------------------------------------------------------------- /api.properties: -------------------------------------------------------------------------------- 1 | main_url="https://proexpense.herokuapp.com" 2 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /release 3 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -keepnames class androidx.lifecycle.ViewModel 23 | -keepclassmembers public class * extends androidx.lifecycle.ViewModel { public (...); } 24 | -keepclassmembers class * { public (...); } 25 | -keepclassmembers class * extends androidx.work.Worker { 26 | public (android.content.Context,androidx.work.WorkerParameters); 27 | } 28 | -keepclassmembers class * extends androidx.work.Worker { 29 | public (android.content.Context,androidx.work.WorkerParameters); 30 | } 31 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/arduia/expense/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.arduia.myacc", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/assets/privacy_policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Privacy Policy- Pro Expense 5 | 6 | 21 | 22 | 23 | 24 | 25 | 26 |

This Privacy Policy applies to Pro Expense Android application.

27 |

User Entered Data

28 |

Pro Expense does not access any data on your device other than its own database, that is its own folder on device's internal storage

29 |

Permission Use

30 |

Pro Expense require following permissions:

31 |

Internet: To check latest version of app

32 |

Changes

33 |

This Privacy Policy may be updated from time to time for any reason without prior notice. 34 | We will notify you of any changes to our Privacy Policy by sending the new Privacy Policy to you via email.

35 | 36 |

Contact Us

37 |

If you have any questions regarding with this Privacy Policy, or have questions about our practices, please contact us via email at dev.arduia@gmail.com.

38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/BackupRepository.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data 2 | 3 | import android.net.Uri 4 | import com.arduia.expense.data.local.BackupEnt 5 | import com.arduia.expense.model.FlowResult 6 | import kotlinx.coroutines.flow.Flow 7 | 8 | interface BackupRepository { 9 | 10 | suspend fun insertBackup(item: BackupEnt) 11 | 12 | suspend fun updateBackup(item: BackupEnt) 13 | 14 | suspend fun deleteBackup(item: BackupEnt) 15 | 16 | suspend fun deleteBackupByID(id: Int) 17 | 18 | fun getBackupAll(): FlowResult> 19 | 20 | fun getBackupByID(id: Int): FlowResult 21 | 22 | fun getBackupByWorkerID(id: String): FlowResult 23 | 24 | fun getItemCount(uri: Uri): FlowResult 25 | 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/CurrencyRepository.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data 2 | 3 | import com.arduia.expense.data.local.CurrencyDto 4 | import com.arduia.expense.model.FlowResult 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface CurrencyRepository { 8 | 9 | fun getCurrencies(): FlowResult> 10 | 11 | fun getSelectedCacheCurrency(): FlowResult 12 | 13 | suspend fun setSelectedCacheCurrency(num: String) 14 | 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/ProExpenseServerRepository.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data 2 | 3 | import com.arduia.expense.data.network.CheckUpdateDto 4 | import com.arduia.expense.data.network.ExpenseVersionDto 5 | import com.arduia.expense.data.network.FeedbackDto 6 | import com.arduia.expense.model.FlowResult 7 | import com.arduia.expense.model.Result 8 | import java.util.concurrent.Flow 9 | 10 | interface ProExpenseServerRepository { 11 | 12 | fun postFeedback(comment: FeedbackDto.Request): FlowResult 13 | 14 | fun getVersionStatus(): FlowResult 15 | 16 | suspend fun getAboutUpdateSync(deviceInfo: CheckUpdateDto.Request): Result 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data 2 | 3 | import android.content.Context 4 | import com.arduia.expense.data.local.AboutUpdateDataModel 5 | import com.arduia.expense.model.FlowResult 6 | import com.arduia.expense.model.Result 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.flow.Flow 9 | 10 | interface SettingsRepository{ 11 | 12 | fun getSelectedLanguage(): FlowResult 13 | 14 | suspend fun setSelectedLanguage(id: String) 15 | 16 | suspend fun getSelectedLanguageSync(): Result 17 | 18 | fun getFirstUser(): FlowResult 19 | 20 | suspend fun getFirstUserSync(): Result 21 | 22 | suspend fun setFirstUser(isFirstUser: Boolean) 23 | 24 | fun getSelectedCurrencyNumber(): FlowResult 25 | 26 | suspend fun getSelectedCurrencyNumberSync(): Result 27 | 28 | suspend fun setSelectedCurrencyNumber(num: String) 29 | 30 | suspend fun setSelectedThemeMode(mode: Int) 31 | 32 | suspend fun getSelectedThemeModeSync(): Result 33 | 34 | fun getUpdateStatus(): FlowResult 35 | 36 | suspend fun setUpdateStatus(status: Int) 37 | 38 | suspend fun getAboutUpdateSync(): Result 39 | 40 | suspend fun setAboutUpdate(info: AboutUpdateDataModel) 41 | 42 | interface Factory{ 43 | fun create(context: Context): SettingsRepository 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/backup/ExpenseBackupSource.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.backup 2 | 3 | import com.arduia.backup.BackupSource 4 | import com.arduia.expense.data.ExpenseRepository 5 | import com.arduia.expense.data.local.ExpenseEnt 6 | import com.arduia.expense.model.awaitValueOrError 7 | import com.arduia.expense.model.data 8 | import com.arduia.expense.model.getDataOrError 9 | import kotlinx.coroutines.flow.first 10 | import kotlinx.coroutines.flow.single 11 | import java.lang.Exception 12 | import javax.inject.Inject 13 | 14 | class ExpenseBackupSource @Inject constructor (private val repo: ExpenseRepository): BackupSource{ 15 | override suspend fun write(item: ExpenseEnt) { 16 | repo.insertExpense(item) 17 | } 18 | 19 | override suspend fun writeAll(items: List) { 20 | repo.insertExpenseAll(items) 21 | } 22 | 23 | override suspend fun readAll(): List { 24 | return repo.getExpenseAllSync().getDataOrError() 25 | } 26 | 27 | override suspend fun totalCountAll(): Int { 28 | return repo.getExpenseTotalCountSync().getDataOrError() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/exception/RepositoryException.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.exception 2 | 3 | import java.lang.Exception 4 | 5 | class RepositoryException(throwable: Throwable?=null) : Exception(throwable) -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/ext/Context.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.ext 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | 6 | 7 | @SuppressWarnings("versionCode") 8 | fun Context.getAppVersionCode(): Long{ 9 | 10 | val info = packageManager.getPackageInfo(packageName, 0) 11 | 12 | return if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){ 13 | info.longVersionCode 14 | }else info.versionCode.toLong() 15 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/ext/Result.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.ext 2 | 3 | import com.arduia.expense.data.exception.RepositoryException 4 | import com.arduia.expense.model.Result 5 | import java.lang.Exception 6 | 7 | inline fun getResultSuccessOrError(fetch: () -> T): Result { 8 | return try { 9 | Result.Success(fetch()) 10 | } catch (e: Exception) { 11 | Result.Error(RepositoryException(e)) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/AboutUpdateDataModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class AboutUpdateDataModel( 6 | @SerializedName("name") 7 | val name: String, 8 | 9 | @SerializedName("code") 10 | val code: Long, 11 | 12 | @SerializedName("change_logs") 13 | val changeLogs: String 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/AmountTypeConverter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.room.TypeConverter 4 | import com.arduia.expense.domain.Amount 5 | 6 | class AmountTypeConverter { 7 | 8 | @TypeConverter 9 | fun fromRawAmount(amount: Long): Amount{ 10 | return Amount.createFromStore(amount) 11 | } 12 | 13 | @TypeConverter 14 | fun toRawAmount(amount: Amount): Long{ 15 | return amount.getStore() 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/BackupDao.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.room.* 4 | import kotlinx.coroutines.flow.Flow 5 | 6 | @Dao 7 | interface BackupDao { 8 | 9 | @Insert(onConflict = OnConflictStrategy.REPLACE) 10 | suspend fun insertBackup(item: BackupEnt) 11 | 12 | @Delete 13 | suspend fun deleteBackup(item: BackupEnt) 14 | 15 | @Query("DELETE FROM backup WHERE backup_id =:id") 16 | fun deleteBackupByID(id: Int) 17 | 18 | @Update 19 | suspend fun updateBackup(item: BackupEnt) 20 | 21 | @Query("SELECT * FROM backup WHERE backup_id =:id ") 22 | fun getBackupByID(id: Int): Flow 23 | 24 | @Query("SELECT * FROM backup WHERE worker_id =:worker_id") 25 | fun getBackupByWorkerID(worker_id: String): Flow 26 | 27 | @Query("SELECT * FROM backup ORDER BY created_date DESC") 28 | fun getBackupAll(): Flow> 29 | 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/BackupEnt.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity(tableName = "backup") 8 | data class BackupEnt( 9 | 10 | @ColumnInfo(name = "backup_id") 11 | @PrimaryKey(autoGenerate = true) 12 | val backupId: Int, 13 | 14 | @ColumnInfo(name = "name") 15 | val name: String, 16 | 17 | @ColumnInfo(name = "path") 18 | val filePath: String, 19 | 20 | @ColumnInfo(name = "created_date") 21 | val createdDate: Long, 22 | 23 | @ColumnInfo(name = "item_total") 24 | var itemTotal: Int, 25 | 26 | @ColumnInfo(name = "worker_id") 27 | val workerId: String, 28 | 29 | @ColumnInfo(name = "is_completed") 30 | var isCompleted : Boolean 31 | 32 | ) 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/CacheDao.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface CacheDao { 6 | 7 | fun getSelectedCurrency(): Flow 8 | 9 | suspend fun setSelectedCurrency(currency: CurrencyDto) 10 | 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/CacheDaoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import kotlinx.coroutines.channels.ConflatedBroadcastChannel 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.asFlow 6 | 7 | object CacheDaoImpl : CacheDao{ 8 | 9 | private val currencyCH = ConflatedBroadcastChannel() 10 | 11 | override fun getSelectedCurrency(): Flow { 12 | return currencyCH.asFlow() 13 | } 14 | 15 | override suspend fun setSelectedCurrency(currency: CurrencyDto) { 16 | currencyCH.send(currency) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/CurrencyDao.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | 5 | interface CurrencyDao { 6 | 7 | fun getCurrencies(): Flow> 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/CurrencyDaoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import android.content.res.AssetManager 4 | import com.google.gson.GsonBuilder 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import java.lang.RuntimeException 8 | import javax.inject.Inject 9 | 10 | class CurrencyDaoImpl @Inject constructor(private val assetManager: AssetManager) : CurrencyDao { 11 | 12 | override fun getCurrencies(): Flow> = flow{ 13 | try { 14 | val file = assetManager.open(CURRENCY_FILE_PATH) 15 | emit(GsonBuilder() 16 | .create() 17 | .fromJson(file.reader(), Currencies::class.java) 18 | .sortedBy { it.rank }) 19 | } catch (e: Exception) { 20 | throw RuntimeException("currencies", e) 21 | } 22 | } 23 | 24 | companion object { 25 | const val CURRENCY_FILE_PATH = "currencies.json" 26 | } 27 | } 28 | 29 | private class Currencies : java.util.ArrayList() -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/CurrencyDto.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class CurrencyDto( 6 | 7 | @SerializedName("rank") 8 | val rank: Int, 9 | 10 | @SerializedName("currency_name") 11 | val name: String, 12 | 13 | @SerializedName("code") 14 | val code: String, 15 | 16 | @SerializedName("symbol") 17 | val symbol: String, 18 | 19 | @SerializedName("number") 20 | val number: String 21 | 22 | ){ 23 | override fun toString(): String { 24 | return "$rank $name $code $symbol $number" 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/DateRangeDataModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class DateRangeDataModel( 6 | @SerializedName("maxDate") 7 | val maxDate: Long, 8 | @SerializedName("minDate") 9 | val minDate: Long 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/ExpenseEnt.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.room.* 4 | import com.arduia.expense.domain.Amount 5 | 6 | @Entity(tableName = ExpenseEnt.TABLE_NAME) 7 | data class ExpenseEnt( 8 | 9 | @PrimaryKey(autoGenerate = true) 10 | @ColumnInfo(name = "expense_id") 11 | val expenseId: Int = 0, 12 | 13 | @ColumnInfo(name = "name") 14 | val name: String?, 15 | 16 | @TypeConverters(AmountTypeConverter::class) 17 | @ColumnInfo(name = "amount") 18 | val amount: Amount, 19 | 20 | @ColumnInfo(name = "category") 21 | val category: Int, 22 | 23 | @ColumnInfo(name = "note") 24 | val note: String?, 25 | 26 | @ColumnInfo(name = "created_date") 27 | val createdDate: Long, 28 | 29 | @ColumnInfo(name = "modified_date") 30 | val modifiedDate: Long 31 | ){ 32 | companion object{ 33 | const val TABLE_NAME = "expense" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/PreferenceStorageDao.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.room.Update 4 | import com.arduia.expense.model.Result 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface PreferenceStorageDao { 8 | 9 | fun getSelectedLanguage(): Flow 10 | 11 | suspend fun getSelectedLanguageSync(): String 12 | 13 | suspend fun setSelectedLanguage(id: String) 14 | 15 | fun getFirstUser(): Flow 16 | 17 | suspend fun getFirstUserSync(): Boolean 18 | 19 | suspend fun setFirstUser(isFirstUser: Boolean) 20 | 21 | fun getSelectedCurrencyNumber(): Flow 22 | 23 | suspend fun getSelectedCurrencyNumberSync(): String 24 | 25 | suspend fun setSelectedCurrencyNumber(num: String) 26 | 27 | suspend fun setSelectedThemeMode(mode: Int) 28 | 29 | suspend fun getSelectedThemeModeSync(): Int 30 | 31 | fun getUpdateStatus(): Flow 32 | 33 | suspend fun setUpdateStatus(status: Int) 34 | 35 | suspend fun getAboutUpdateSync(): AboutUpdateDataModel 36 | 37 | suspend fun setAboutUpdate(info: AboutUpdateDataModel) 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/local/UpdateStatusDataModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.local 2 | 3 | import androidx.annotation.IntDef 4 | import androidx.annotation.IntRange 5 | 6 | class UpdateStatusDataModel { 7 | companion object { 8 | const val STATUS_NO_UPDATE = -1 9 | const val STATUS_NORMAL_UPDATE = 1 10 | const val STATUS_CRITICAL_UPDATE = 2 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/network/CheckUpdateDto.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.network 2 | 3 | import com.arduia.expense.data.local.AboutUpdateDataModel 4 | import com.google.gson.annotations.SerializedName 5 | 6 | class CheckUpdateDto { 7 | 8 | data class Request( 9 | @SerializedName("device_version_code") 10 | val currentVersion: Long, 11 | 12 | @SerializedName("key") 13 | val key: String 14 | ) 15 | 16 | data class Response( 17 | @SerializedName("is_should_update") 18 | val isShouldUpdate: Boolean, 19 | 20 | @SerializedName("is_critical") 21 | val isCriticalUpdate: Boolean = false, 22 | 23 | @SerializedName("info") 24 | val info: AboutUpdateDataModel? = null 25 | ) 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/network/ExpenseNetworkDao.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.network 2 | 3 | import retrofit2.Call 4 | import retrofit2.http.Body 5 | import retrofit2.http.GET 6 | import retrofit2.http.POST 7 | 8 | 9 | interface ExpenseNetworkDao { 10 | @GET("/api/version_status.json") 11 | suspend fun getVersionStatus(): ExpenseVersionDto 12 | 13 | @POST("/api/feedback_submit.json") 14 | suspend fun postFeedback(@Body feedback: FeedbackDto.Request): FeedbackDto.Response 15 | 16 | @POST("/api/check_update_info.json") 17 | suspend fun getCheckUpdateInfo(@Body deviceInfo: CheckUpdateDto.Request): CheckUpdateDto.Response 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/network/ExpenseVersionDto.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.network 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | data class ExpenseVersionDto( 6 | @SerializedName("version_code") 7 | val version_code: Int, 8 | 9 | @SerializedName("version_name") 10 | val version_name: String 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/data/network/FeedbackDto.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.data.network 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | class FeedbackDto{ 6 | 7 | data class Request( 8 | @SerializedName("name") 9 | val name: String, 10 | 11 | @SerializedName("email") 12 | val email: String, 13 | 14 | @SerializedName("comment") 15 | val comment: String, 16 | 17 | @SerializedName("key") 18 | val key: String 19 | ) 20 | 21 | data class Response( 22 | @SerializedName("code") 23 | val code: Int, 24 | 25 | @SerializedName("message") 26 | val message: String 27 | ) 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AbstractBackupModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import com.arduia.backup.BackupSheet 4 | import com.arduia.backup.BackupSource 5 | import com.arduia.backup.FileNameGenerator 6 | import com.arduia.backup.generator.BackupNameGenerator 7 | import com.arduia.expense.backup.schema.BackupSchema 8 | import com.arduia.expense.data.backup.ExpenseBackupSheet 9 | import com.arduia.expense.data.backup.ExpenseBackupSource 10 | import com.arduia.expense.data.backup.SchemaBackupSheet 11 | import com.arduia.expense.data.backup.SchemaBackupSource 12 | import com.arduia.expense.data.local.BackupEnt 13 | import com.arduia.expense.data.local.ExpenseEnt 14 | import dagger.Binds 15 | import dagger.Module 16 | import dagger.Provides 17 | import dagger.hilt.InstallIn 18 | import dagger.hilt.components.SingletonComponent 19 | import javax.inject.Singleton 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | abstract class AbstractBackupModule { 24 | 25 | @Binds 26 | abstract fun bindSchemaSource(impl: SchemaBackupSource): BackupSource 27 | 28 | @Binds 29 | abstract fun bindSchemaBackupSheet(impl: SchemaBackupSheet): BackupSheet 30 | 31 | @Binds 32 | abstract fun bindExpenseSource(impl: ExpenseBackupSource): BackupSource 33 | 34 | @Binds 35 | abstract fun bindExpenseBackupSheet(impl: ExpenseBackupSheet): BackupSheet 36 | 37 | 38 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AbstractDomainModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import com.arduia.expense.ui.common.category.ExpenseCategoryProvider 4 | import com.arduia.expense.ui.statistics.CategoryAnalyzer 5 | import com.arduia.expense.ui.statistics.CategoryAnalyzerImpl 6 | import dagger.Binds 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.android.components.ActivityComponent 11 | import dagger.hilt.components.SingletonComponent 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | abstract class AbstractDomainModule { 16 | 17 | @Binds 18 | abstract fun provideCategoryStatisticAnalyzer(impl: CategoryAnalyzerImpl): CategoryAnalyzer 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AbstractExpenseModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.content.Context 4 | import com.arduia.expense.ui.common.category.ExpenseCategoryProvider 5 | import com.arduia.expense.ui.common.category.ExpenseCategoryProviderImpl 6 | import com.arduia.expense.ui.common.language.LanguageProvider 7 | import com.arduia.expense.ui.common.language.LanguageProviderImpl 8 | import com.arduia.expense.ui.home.ExpenseDayNameProvider 9 | import com.arduia.expense.ui.home.ExpenseRateCalculator 10 | import com.arduia.expense.ui.home.ExpenseRateCalculatorFactory 11 | import com.arduia.graph.DayNameProvider 12 | import dagger.Binds 13 | import dagger.Module 14 | import dagger.Provides 15 | import dagger.hilt.InstallIn 16 | import dagger.hilt.android.components.ActivityComponent 17 | import dagger.hilt.android.qualifiers.ActivityContext 18 | import dagger.hilt.android.scopes.ActivityScoped 19 | import dagger.hilt.components.SingletonComponent 20 | 21 | @Module 22 | @InstallIn(SingletonComponent::class) 23 | abstract class AbstractExpenseModule { 24 | 25 | @Binds 26 | abstract fun provideExpenseCategory(impl: ExpenseCategoryProviderImpl): ExpenseCategoryProvider 27 | 28 | @Binds 29 | abstract fun provideLanguage(impl: LanguageProviderImpl): LanguageProvider 30 | 31 | @Binds 32 | abstract fun provideExpenseCalculator(impl: ExpenseRateCalculatorFactory): ExpenseRateCalculator.Factory 33 | 34 | @Binds 35 | abstract fun provideDataNames(impl: ExpenseDayNameProvider): DayNameProvider 36 | 37 | } 38 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AbstractFormattingModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.content.Context 4 | import com.arduia.expense.ui.common.formatter.* 5 | import dagger.Binds 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.components.ActivityComponent 10 | import dagger.hilt.android.qualifiers.ActivityContext 11 | import dagger.hilt.components.SingletonComponent 12 | 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | abstract class AbstractFormattingModule { 16 | 17 | @Binds 18 | @StatisticDateRange 19 | abstract fun provideStatisticDateRangeFormatter(impl: StatisticDateRangeFormatter): DateRangeFormatter 20 | 21 | @Binds 22 | @MonthlyDateRange 23 | abstract fun provideMonthlyDateRangeFormatter(impl: MonthDateRangeFormatter): DateRangeFormatter 24 | 25 | @Binds 26 | abstract fun provideDateFormatter(impl: ExpenseRecentDateFormatter): DateFormatter 27 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AbstractMapperModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import com.arduia.core.arch.Mapper 4 | import com.arduia.expense.data.local.AboutUpdateDataModel 5 | import com.arduia.expense.data.local.BackupEnt 6 | import com.arduia.expense.ui.about.AboutUpdateUiModel 7 | import com.arduia.expense.ui.expenselogs.ExpenseUiModelMapperFactory 8 | import com.arduia.expense.ui.expenselogs.ExpenseUiModelMapperFactoryImpl 9 | import com.arduia.expense.ui.about.AboutUpdateUiModelMapper 10 | import com.arduia.expense.ui.backup.BackupUiModelMapper 11 | import com.arduia.expense.ui.expenselogs.ExpenseEntToLogVoMapper.*; 12 | import com.arduia.expense.ui.expenselogs.ExpenseEntToLogVoMapperFactory 13 | import com.arduia.expense.ui.backup.BackupUiModel 14 | import dagger.Binds 15 | import dagger.Module 16 | import dagger.hilt.InstallIn 17 | import dagger.hilt.android.components.ActivityComponent 18 | import dagger.hilt.components.SingletonComponent 19 | 20 | @Module 21 | @InstallIn(SingletonComponent::class) 22 | abstract class AbstractMapperModule { 23 | 24 | @Binds 25 | abstract fun bindExpenseLogVoMapperFactory(impl: ExpenseUiModelMapperFactoryImpl): 26 | ExpenseUiModelMapperFactory 27 | 28 | @Binds 29 | abstract fun bindExpenseEntToLogMapperFactory(impl: ExpenseEntToLogVoMapperFactoryImpl): 30 | ExpenseEntToLogVoMapperFactory 31 | 32 | @Binds 33 | abstract fun bindAboutUpdateUiToDataMapper(impl: AboutUpdateUiModelMapper): 34 | Mapper 35 | 36 | @Binds 37 | abstract fun bindBackupVoMapper(impl: BackupUiModelMapper): Mapper 38 | 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AdapterModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.content.Context 4 | import android.view.LayoutInflater 5 | import com.arduia.expense.ui.backup.BackupListAdapter 6 | import com.arduia.expense.ui.expenselogs.ExpenseLogAdapter 7 | import com.arduia.expense.ui.home.RecentListAdapter 8 | import dagger.Module 9 | import dagger.Provides 10 | import dagger.hilt.InstallIn 11 | import dagger.hilt.android.components.FragmentComponent 12 | import dagger.hilt.android.qualifiers.ActivityContext 13 | 14 | @Module 15 | @InstallIn(FragmentComponent::class) 16 | object AdapterModule{ 17 | 18 | @Provides 19 | fun provideLayoutInflater(@ActivityContext context: Context) 20 | : LayoutInflater = LayoutInflater.from(context) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/AnimationModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import androidx.navigation.NavOptions 4 | import com.arduia.expense.R 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Qualifier 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object AnimationModule { 14 | 15 | @Provides 16 | @LefSideNavOption 17 | fun provideLeftSlideNavOption(): NavOptions = 18 | NavOptions.Builder() 19 | //For Transaction Fragment 20 | .setEnterAnim(R.anim.expense_enter_left) 21 | .setPopExitAnim(R.anim.expense_exit_right) 22 | //For Home Fragment 23 | .setExitAnim(R.anim.nav_default_exit_anim) 24 | .setPopEnterAnim(R.anim.nav_default_enter_anim) 25 | .setLaunchSingleTop(true) 26 | .build() 27 | 28 | @Provides 29 | @TopDropNavOption 30 | fun provideTopDropNavOption(): NavOptions = 31 | NavOptions.Builder() 32 | //For Entry Fragment 33 | .setEnterAnim(R.anim.pop_down_up) 34 | .setPopExitAnim(R.anim.pop_up_down) 35 | //For Home Fragment 36 | .setExitAnim(android.R.anim.fade_out) 37 | .setPopEnterAnim(R.anim.nav_default_enter_anim) 38 | .setLaunchSingleTop(true) 39 | .build() 40 | 41 | } 42 | 43 | @Qualifier 44 | @MustBeDocumented 45 | @Retention(AnnotationRetention.BINARY) 46 | annotation class TopDropNavOption 47 | 48 | @Qualifier 49 | @MustBeDocumented 50 | @Retention(AnnotationRetention.BINARY) 51 | annotation class LefSideNavOption 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/BackgroundModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.content.Context 4 | import androidx.work.WorkManager 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | import javax.inject.Singleton 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object BackgroundModule { 15 | 16 | @Provides 17 | @Singleton 18 | fun provideWorkManager(@ApplicationContext context: Context) = WorkManager.getInstance(context) 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/BackupMessagingModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.app.Activity 4 | import com.arduia.expense.ui.BackupMessageReceiver 5 | import com.arduia.expense.ui.MainActivity 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.android.components.ActivityComponent 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object BackupMessagingModule{ 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/BackupModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.app.Activity 4 | import com.arduia.backup.BackupSheet 5 | import com.arduia.backup.ExcelBackup 6 | import com.arduia.backup.FileNameGenerator 7 | import com.arduia.backup.generator.BackupNameGenerator 8 | import com.arduia.expense.backup.schema.BackupSchema 9 | import com.arduia.expense.data.ExpenseRepository 10 | import com.arduia.expense.data.backup.ExpenseBackupSheet 11 | import com.arduia.expense.data.backup.ExpenseBackupSource 12 | import com.arduia.expense.data.local.ExpenseEnt 13 | import com.arduia.expense.ui.BackupMessageReceiver 14 | import dagger.Module 15 | import dagger.Provides 16 | import dagger.hilt.InstallIn 17 | import dagger.hilt.components.SingletonComponent 18 | import javax.inject.Qualifier 19 | import javax.inject.Singleton 20 | 21 | 22 | @Module 23 | @InstallIn(SingletonComponent::class) 24 | object BackupModule { 25 | 26 | 27 | @Provides 28 | @Singleton 29 | fun provideExcelBackup( 30 | expenseSheet: BackupSheet, 31 | schemaSheet: BackupSheet 32 | ) = 33 | ExcelBackup.Builder() 34 | .addBackupSheet(expenseSheet) 35 | .addBackupSheet(schemaSheet) 36 | .build() 37 | 38 | @Provides 39 | @Singleton 40 | @BackupNameGen 41 | fun provideBackupNameGen(): FileNameGenerator = BackupNameGenerator() 42 | 43 | } 44 | 45 | @Qualifier 46 | @MustBeDocumented 47 | @Retention(AnnotationRetention.BINARY) 48 | annotation class BackupNameGen 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/ContentProviderModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.content.ContentResolver 4 | import android.content.Context 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import dagger.hilt.components.SingletonComponent 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | object ContentProviderModule { 14 | 15 | @Provides 16 | fun provideContentResolver(@ApplicationContext context: Context): ContentResolver = context.contentResolver 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/DatabaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.app.Application 4 | import androidx.room.InvalidationTracker 5 | import androidx.room.Room 6 | import com.arduia.expense.data.local.ProExpenseDatabase 7 | import com.arduia.expense.data.local.BackupDao 8 | import com.arduia.expense.data.local.ExpenseDao 9 | import dagger.Module 10 | import dagger.Provides 11 | import dagger.hilt.InstallIn 12 | import dagger.hilt.components.SingletonComponent 13 | import javax.inject.Singleton 14 | 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object DatabaseModule{ 18 | 19 | @Provides 20 | @Singleton 21 | fun provideAccDatabase(application: Application): ProExpenseDatabase { 22 | return Room.databaseBuilder(application, ProExpenseDatabase::class.java, "accounting.db") 23 | .addMigrations(ProExpenseDatabase.MIGRATION_3_4, ProExpenseDatabase.MIGRATION_4_6) 24 | .build() 25 | } 26 | 27 | @Provides 28 | fun provideAccountingDatabaseTracker(db: ProExpenseDatabase): InvalidationTracker{ 29 | return db.invalidationTracker 30 | } 31 | 32 | @Provides 33 | @Singleton 34 | fun provideAccDao(accDb: ProExpenseDatabase): ExpenseDao = accDb.expenseDao 35 | 36 | @Provides 37 | @Singleton 38 | fun provideBackupDao(db: ProExpenseDatabase): BackupDao = db.backupDao 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/NavHostModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.app.Activity 4 | import com.arduia.expense.ui.MainHost 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ActivityComponent 9 | import dagger.hilt.android.scopes.ActivityScoped 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | @Module 13 | @InstallIn(ActivityComponent::class) 14 | object NavHostModule { 15 | 16 | @Provides 17 | @ActivityScoped 18 | fun provideMainHost( app: Activity) = app as MainHost 19 | 20 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import com.arduia.expense.BuildConfig 4 | import com.arduia.expense.data.network.ExpenseNetworkDao 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import retrofit2.Retrofit 10 | import retrofit2.converter.gson.GsonConverterFactory 11 | import javax.inject.Singleton 12 | 13 | 14 | @Module 15 | @InstallIn(SingletonComponent::class) 16 | object NetworkModule{ 17 | 18 | @Provides 19 | @Singleton 20 | fun provideRetrofit(): Retrofit = Retrofit.Builder() 21 | .baseUrl(BuildConfig.BASE_URL) 22 | .addConverterFactory(GsonConverterFactory.create()) 23 | .build() 24 | 25 | @Provides 26 | @Singleton 27 | fun provideNetworkDao(retrofit: Retrofit): ExpenseNetworkDao = 28 | retrofit.create(ExpenseNetworkDao::class.java) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/di/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.di 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import android.content.res.AssetManager 6 | import androidx.room.InvalidationTracker 7 | import com.arduia.backup.ExcelBackup 8 | import com.arduia.expense.data.* 9 | import com.arduia.expense.data.local.* 10 | import com.arduia.expense.data.network.ExpenseNetworkDao 11 | import dagger.Binds 12 | import dagger.Module 13 | import dagger.Provides 14 | import dagger.hilt.InstallIn 15 | import dagger.hilt.android.qualifiers.ApplicationContext 16 | import dagger.hilt.components.SingletonComponent 17 | import javax.inject.Singleton 18 | 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object RepositoryModule { 22 | 23 | @Provides 24 | fun provideAssetManager(@ApplicationContext context: Context): AssetManager = context.assets 25 | 26 | @Provides 27 | fun provideCacheDao(): CacheDao = CacheDaoImpl 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/Amount.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain 2 | 3 | import timber.log.Timber 4 | import java.math.BigDecimal 5 | import java.math.MathContext 6 | import java.math.RoundingMode 7 | import kotlin.math.roundToInt 8 | 9 | class Amount: ExpenseStore(DataStoreExchangeRate){ 10 | 11 | override fun toString(): String { 12 | return "Amount(storeValue = $storeValue)" 13 | } 14 | 15 | companion object{ 16 | fun createFromActual(actual: BigDecimal) = Amount().apply { 17 | 18 | val storeValue = actual.multiply(BigDecimal(rate.getRate())).apply { 19 | setScale(0, RoundingMode.FLOOR) 20 | } 21 | setStore( storeValue.longValueExact()) 22 | } 23 | 24 | fun createFromStore(store: Long) = Amount().apply { 25 | setStore(store) 26 | } 27 | } 28 | 29 | } 30 | 31 | operator fun Amount.times(multiplier: Amount): Amount{ 32 | val left = this.getStore() 33 | val right = multiplier.getStore() 34 | val result = left * right 35 | this.setStore(result) 36 | return this 37 | } 38 | 39 | operator fun Amount.times(time: Number): Amount{ 40 | val left = this.getStore() 41 | val result = (left * time.toInt()) 42 | this.setStore(result) 43 | return this 44 | } 45 | 46 | operator fun Amount.plus(amount: Amount): Amount{ 47 | val result = this.getStore() + amount.getStore() 48 | setStore(result) 49 | return this 50 | } 51 | 52 | fun Amount.getActualAsFloat(): Float{ 53 | return this.getActual().setScale(2, RoundingMode.DOWN).toFloat() 54 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/DataStoreExchangeRate.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain 2 | 3 | import com.arduia.currencystore.Rate 4 | 5 | object DataStoreExchangeRate: Rate { 6 | override fun getRate(): Long = 100L 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/ExpenseLogItemEnt.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain 2 | 3 | data class ExpenseLogItemEnt( 4 | val expenseId: Int = 0, 5 | 6 | val name: String?, 7 | 8 | val amount: Amount, 9 | 10 | val category: Int, 11 | 12 | val note: String?, 13 | 14 | val createdDate: Long, 15 | 16 | val modifiedDate: Long 17 | ) 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/ExpenseStore.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain 2 | 3 | import com.arduia.currencystore.Rate 4 | import com.arduia.currencystore.Store 5 | import timber.log.Timber 6 | import java.lang.Exception 7 | import java.math.BigDecimal 8 | 9 | abstract class ExpenseStore(rate: Rate) : Store(rate) { 10 | 11 | protected var storeValue:Long = 0 12 | 13 | override fun getActual(): BigDecimal { 14 | validateRateOrError(rate) 15 | return BigDecimal(storeValue.toDouble()/ rate.getRate()) 16 | } 17 | 18 | override fun getStore() = storeValue 19 | 20 | override fun setStore(value: Long) { 21 | this.storeValue = value 22 | } 23 | 24 | } 25 | 26 | private fun validateRateOrError(rate: Rate) { 27 | if (rate.getRate() <= 0) throw Exception("Invalid Rate Value (${rate.getRate()}). It should be greater than 0") 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/filter/DateRange.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain.filter 2 | 3 | interface DateRange : Range -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/filter/ExpenseDateRange.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain.filter 2 | 3 | import com.arduia.expense.ui.common.exception.validateDateRange 4 | 5 | class ExpenseDateRange(override val start: Long, override val end: Long) : DateRange{ 6 | init { 7 | validateDateRange(start, end) 8 | } 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/filter/ExpenseLogFilterInfo.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain.filter 2 | 3 | import com.arduia.expense.ui.common.filter.Sorting 4 | 5 | data class ExpenseLogFilterInfo( 6 | val dateRangeLimit: DateRange, 7 | val dateRangeSelected: DateRange, 8 | val sorting: Sorting 9 | ) { 10 | 11 | class Builder { 12 | private var limit: DateRange = ExpenseDateRange(0, 0) 13 | private var selected: DateRange = limit 14 | private var sorting = Sorting.DESC 15 | 16 | fun setDateLimit(range: DateRange): Builder { 17 | this.limit = range 18 | return this 19 | } 20 | 21 | fun setSelectedLimit(range: DateRange): Builder { 22 | this.selected = range 23 | return this 24 | } 25 | 26 | fun setSorting(sorting: Sorting): Builder { 27 | this.sorting 28 | return this 29 | } 30 | 31 | fun build() = 32 | ExpenseLogFilterInfo( 33 | dateRangeLimit = limit, 34 | dateRangeSelected = selected, 35 | sorting = sorting 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/domain/filter/Range.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.domain.filter 2 | 3 | interface Range { 4 | val start: S 5 | val end: E 6 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/model/FlowResult.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.model 2 | 3 | import kotlinx.coroutines.CancellationException 4 | import kotlinx.coroutines.flow.* 5 | import kotlinx.coroutines.runBlocking 6 | 7 | typealias FlowResult = Flow> 8 | 9 | fun FlowResult.awaitValueOrError(): T = runBlocking { 10 | 11 | var firstSuccess: T? = null 12 | 13 | try { 14 | collect { 15 | if (it is Result.Success) { 16 | firstSuccess = it.data 17 | throw AbortFlowException() 18 | } 19 | if (it is Result.Error) throw Exception(it.exception) 20 | } 21 | } catch (e: AbortFlowException) { 22 | } 23 | 24 | return@runBlocking firstSuccess ?: throw Exception("Await Value: Empty Result") 25 | } 26 | 27 | class AbortFlowException : CancellationException() 28 | 29 | fun FlowResult.onSuccess(success: (data: T) -> Unit): FlowResult { 30 | return onEach { 31 | if (it is SuccessResult) success.invoke(it.data) 32 | } 33 | } 34 | 35 | fun FlowResult.onLoading(loading: (Boolean) -> Unit): FlowResult { 36 | return onEach { 37 | if (it is LoadingResult) loading.invoke(true) else loading.invoke(false) 38 | } 39 | } 40 | 41 | fun FlowResult.onError(error: (Exception) -> Unit): FlowResult { 42 | return onEach { 43 | if (it is ErrorResult) error.invoke(it.exception) 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/BackupMessageReceiver.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui 2 | 3 | import java.util.* 4 | 5 | /** 6 | * Received Backup Tasks and show Progress state 7 | * with Toast message. Must be implemented by Host Activity 8 | */ 9 | interface BackupMessageReceiver{ 10 | 11 | fun registerBackupTaskID(id: UUID) 12 | 13 | fun unregisterBackupTaskID(id: UUID) 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/MainHost.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui 2 | 3 | import androidx.fragment.app.Fragment 4 | 5 | /** 6 | * Communication interface between host activity and 7 | * it's child view such as Fragments. 8 | * 9 | * Received Add button onClickListener, Toast message 10 | * to not overlap Floating Action Button and Toast. 11 | */ 12 | 13 | interface MainHost{ 14 | 15 | val defaultSnackBarDuration: Int 16 | 17 | fun showAddButton() 18 | 19 | fun showAddButtonInstantly() 20 | 21 | fun hideAddButton() 22 | 23 | fun setAddButtonClickListener(listener: (()-> Unit)?) 24 | 25 | fun showSnackMessage(message: String, duration: Int = defaultSnackBarDuration) 26 | 27 | } 28 | 29 | val Fragment.mainHost 30 | get() = requireActivity() as MainHost -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui 2 | 3 | import androidx.lifecycle.* 4 | import androidx.work.OneTimeWorkRequestBuilder 5 | import androidx.work.WorkManager 6 | import com.arduia.expense.data.CurrencyRepository 7 | import com.arduia.expense.data.SettingsRepository 8 | import com.arduia.expense.data.update.CheckAboutUpdateWorker 9 | import com.arduia.expense.model.Result 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.flowOn 13 | import kotlinx.coroutines.flow.launchIn 14 | import kotlinx.coroutines.flow.onEach 15 | import kotlinx.coroutines.launch 16 | import timber.log.Timber 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class MainViewModel @Inject constructor( 21 | private val settingRepo: SettingsRepository, 22 | private val currencyRepo: CurrencyRepository, 23 | private val workManager: WorkManager 24 | ) : ViewModel(), LifecycleObserver { 25 | 26 | init { 27 | observeAndCacheSelectedCurrency() 28 | } 29 | 30 | private fun observeAndCacheSelectedCurrency() { 31 | settingRepo.getSelectedCurrencyNumber() 32 | .flowOn(Dispatchers.IO) 33 | .onEach { 34 | if (it is Result.Success) { 35 | currencyRepo.setSelectedCacheCurrency(it.data) 36 | } 37 | } 38 | .launchIn(viewModelScope) 39 | } 40 | 41 | 42 | @OnLifecycleEvent(Lifecycle.Event.ON_CREATE) 43 | private fun startCheckAboutUpdateWork() { 44 | val checkVersionRequest = OneTimeWorkRequestBuilder() 45 | .build() 46 | workManager.enqueue(checkVersionRequest) 47 | Timber.d("startCheckUpdate") 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/NavBaseFragment.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import java.lang.Exception 9 | 10 | /** 11 | * Base class for Fragment which use drawer navigation 12 | */ 13 | abstract class NavBaseFragment : Fragment() { 14 | 15 | private var _navDrawer: NavigationDrawer? = null 16 | protected val navigationDrawer get() = _navDrawer!! 17 | 18 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 19 | super.onViewCreated(view, savedInstanceState) 20 | _navDrawer = (requireActivity() as? NavigationDrawer) 21 | ?: throw Exception("Fragment's host Activity must implement NavigationDrawer interface!") 22 | } 23 | 24 | override fun onDestroyView() { 25 | super.onDestroyView() 26 | _navDrawer = null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/NavigationDrawer.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui 2 | 3 | /** 4 | * Interface between host Activity and child fragments to make Host Activity's drawer actions. 5 | */ 6 | interface NavigationDrawer { 7 | 8 | fun openDrawer() 9 | 10 | fun closeDrawer() 11 | 12 | fun lockDrawer() 13 | 14 | fun unlockDrawer() 15 | 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/about/AboutUpdateDialog.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.about 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AlertDialog 5 | import com.arduia.expense.R 6 | import com.arduia.expense.databinding.FragAboutUpdateDialogBinding 7 | 8 | class AboutUpdateDialog(ctx: Context): AlertDialog(ctx) { 9 | 10 | private var _binding: FragAboutUpdateDialogBinding? = null 11 | private val binding get() = _binding!! 12 | 13 | private lateinit var data: AboutUpdateUiModel 14 | 15 | private var onInstallClickListener: (()-> Unit )? = null 16 | 17 | init { 18 | _binding = FragAboutUpdateDialogBinding.inflate(layoutInflater) 19 | setView(binding.root) 20 | setButton(BUTTON_POSITIVE, context.getString(R.string.install)){ _, _ -> 21 | onInstallClickListener?.invoke() 22 | } 23 | setButton(BUTTON_NEGATIVE,context.getString(R.string.cancel)){_,_ ->} 24 | setTitle(R.string.about_update) 25 | setIcon(R.drawable.ic_update) 26 | } 27 | 28 | fun show(data: AboutUpdateUiModel) { 29 | this.data = data 30 | bindData() 31 | show() 32 | } 33 | 34 | private fun bindData(){ 35 | with(binding){ 36 | tvVersionName.text = data.versionName 37 | tvChangeLog.text = data.changeLogs 38 | tvVersionCode.text = data.versionCode 39 | } 40 | } 41 | 42 | fun setOnInstallClickListener(listener: (()-> Unit)?){ 43 | this.onInstallClickListener = listener 44 | } 45 | 46 | override fun onDetachedFromWindow() { 47 | super.onDetachedFromWindow() 48 | _binding = null 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/about/AboutUpdateUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.about 2 | 3 | data class AboutUpdateUiModel( 4 | val versionName: String, 5 | val versionCode: String, 6 | val changeLogs: String 7 | ) 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/about/AboutUpdateUiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.about 2 | 3 | import com.arduia.core.arch.Mapper 4 | import com.arduia.expense.data.local.AboutUpdateDataModel 5 | import com.arduia.expense.ui.about.AboutUpdateUiModel 6 | import javax.inject.Inject 7 | 8 | class AboutUpdateUiModelMapper @Inject constructor() : 9 | Mapper { 10 | override fun map(input: AboutUpdateDataModel): AboutUpdateUiModel { 11 | return AboutUpdateUiModel( 12 | versionName = input.name, 13 | versionCode = "(${input.code})", 14 | changeLogs = input.changeLogs 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/backup/BackupMapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.backup 2 | 3 | import android.content.Context 4 | import com.arduia.core.arch.Mapper 5 | import com.arduia.expense.R 6 | import com.arduia.expense.data.local.BackupEnt 7 | import com.arduia.expense.di.IntegerDecimal 8 | import com.arduia.expense.ui.common.formatter.DateFormatter 9 | import dagger.hilt.android.qualifiers.ActivityContext 10 | import dagger.hilt.android.qualifiers.ApplicationContext 11 | import java.text.DecimalFormat 12 | import javax.inject.Inject 13 | 14 | class BackupUiModelMapper @Inject constructor( 15 | @ApplicationContext context: Context, 16 | private val dateFormatter: DateFormatter, 17 | @IntegerDecimal private val numberFormat: DecimalFormat 18 | ) : 19 | Mapper { 20 | 21 | private val itemSuffix = context.getString(R.string.single_item_suffix) 22 | private val multiItemSuffix = context.getString(R.string.multi_item_suffix) 23 | 24 | 25 | override fun map(input: BackupEnt) = 26 | BackupUiModel( 27 | id = input.backupId, 28 | name = input.name, 29 | date = dateFormatter.format(input.createdDate), 30 | items = numberFormat.format(input.itemTotal) + " " + if (input.itemTotal > 0) multiItemSuffix else itemSuffix, 31 | onProgress = input.isCompleted.not() 32 | ) 33 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/backup/BackupUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.backup 2 | 3 | data class BackupUiModel( 4 | val name: String, 5 | val id: Int, 6 | val date: String, 7 | val items: String, 8 | val onProgress: Boolean 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/backup/ExportViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.backup 2 | 3 | import android.app.Application 4 | import android.net.Uri 5 | import androidx.lifecycle.* 6 | import androidx.work.Data 7 | import androidx.work.OneTimeWorkRequestBuilder 8 | import androidx.work.WorkManager 9 | import com.arduia.backup.FileNameGenerator 10 | import com.arduia.expense.data.backup.ExportWorker 11 | import com.arduia.expense.di.BackupNameGen 12 | import com.arduia.mvvm.BaseLiveData 13 | import com.arduia.mvvm.post 14 | import com.arduia.mvvm.set 15 | import dagger.hilt.android.lifecycle.HiltViewModel 16 | import javax.inject.Inject 17 | 18 | @HiltViewModel 19 | class ExportViewModel @Inject constructor( 20 | @BackupNameGen 21 | private val fileNameGen: FileNameGenerator, 22 | private val workManager: WorkManager 23 | ) : ViewModel() { 24 | 25 | private val _exportFileName = BaseLiveData() 26 | val exportFileName = _exportFileName.asLiveData() 27 | 28 | init { 29 | _exportFileName set fileNameGen.generate() 30 | } 31 | 32 | fun exportDataTo(fileName: String, fileUri: Uri) { 33 | 34 | val inputUriData = Data.Builder() 35 | .putString(ExportWorker.FILE_URI, fileUri.toString()) 36 | .putString(ExportWorker.FILE_NAME, fileName) 37 | .build() 38 | 39 | val exportRequest = OneTimeWorkRequestBuilder() 40 | .setInputData(inputUriData) 41 | .build() 42 | workManager.enqueue(exportRequest) 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/category/ExpenseCategoryProvider.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.category 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | interface ExpenseCategoryProvider{ 6 | 7 | fun getCategoryList(): List 8 | 9 | @DrawableRes 10 | fun getCategoryDrawableByID(id: Int):Int 11 | 12 | fun getCategoryByID(id: Int): ExpenseCategory 13 | 14 | fun getIndexByCategory(category: ExpenseCategory):Int 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/exception/DateRangeCheck.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.exception 2 | 3 | import java.lang.Exception 4 | 5 | fun validateDateRange(start: Long, end: Long){ 6 | if(start< 0 || end < 0) throw Exception("Invalid Date Range: start($start) or end($end) must be greater or equal zero.") 7 | if(start > end) throw Exception("Invalid Date Range: start($start) should be less than or equal end($end).") 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/expense/ExpenseDetailUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.expense 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | data class ExpenseDetailUiModel( 6 | val id: Int, 7 | val name: String, 8 | val date: String, 9 | @DrawableRes 10 | val category: Int, 11 | val symbol: String, 12 | val amount: String, 13 | val finance: String, 14 | val note: String 15 | ) 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/ext/Activity.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.ext 2 | 3 | import android.app.ActivityOptions 4 | import androidx.fragment.app.Fragment 5 | import com.arduia.expense.R 6 | 7 | fun Fragment.restartActivity() { 8 | val currentActivity = requireActivity() 9 | val intent = currentActivity.intent 10 | currentActivity.finish() 11 | val animationBundle = 12 | ActivityOptions.makeCustomAnimation( 13 | requireContext(), 14 | R.anim.expense_enter_left, android.R.anim.fade_out 15 | ).toBundle() 16 | startActivity(intent, animationBundle) 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/ext/CalendarExt.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.ext 2 | 3 | import java.util.* 4 | 5 | fun Calendar.setDayAsStart(): Calendar { 6 | this[Calendar.HOUR_OF_DAY] = 0 7 | this[Calendar.MINUTE] = 0 8 | this[Calendar.MILLISECOND] = 0 9 | 10 | return this 11 | } 12 | 13 | fun Calendar.setDayAsEnd(): Calendar { 14 | this[Calendar.HOUR_OF_DAY] = 23 15 | this[Calendar.MINUTE] = 59 16 | this[Calendar.MILLISECOND] = 5900 17 | return this 18 | } 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/ext/ColorList.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import androidx.annotation.AttrRes 6 | import androidx.annotation.ColorInt 7 | import androidx.annotation.ColorRes 8 | import androidx.core.content.ContextCompat 9 | import androidx.core.content.res.use 10 | import androidx.fragment.app.Fragment 11 | 12 | fun Fragment.getColorList(@ColorRes color: Int) = ContextCompat.getColorStateList(requireContext(), color) 13 | 14 | @ColorInt 15 | fun Context.themeColor( 16 | @AttrRes themeAttrs: Int 17 | ): Int{ 18 | return obtainStyledAttributes( 19 | intArrayOf(themeAttrs) 20 | ).use { 21 | it.getColor(0, Color.MAGENTA) 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/filter/DateRangeSortingEnt.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.filter 2 | 3 | import com.arduia.expense.domain.filter.DateRange 4 | import com.arduia.expense.ui.common.exception.validateDateRange 5 | 6 | data class DateRangeSortingEnt(val dateRange: DateRange, val sorting: Sorting = Sorting.DESC) 7 | 8 | enum class Sorting { 9 | ASC, DESC 10 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/formatter/DateFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.formatter 2 | 3 | interface DateFormatter { 4 | fun format(time: Long): String 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/formatter/DateRangeFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.formatter 2 | 3 | interface DateRangeFormatter { 4 | 5 | fun format(start: Long, end: Long): String 6 | 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/formatter/ExpenseDateFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.formatter 2 | 3 | import java.util.* 4 | 5 | abstract class ExpenseDateFormatter() : DateFormatter { 6 | private val tmpCalendar = Calendar.getInstance() 7 | private val todayCalendar = Calendar.getInstance() 8 | 9 | @Synchronized 10 | override fun format(time: Long): String { 11 | tmpCalendar.timeInMillis = time 12 | 13 | val isSameYear = (tmpCalendar[Calendar.YEAR] == todayCalendar[Calendar.YEAR]) 14 | val isSameMonth = (tmpCalendar[Calendar.MONTH] == todayCalendar[Calendar.MONTH]) 15 | val isSameDay = (tmpCalendar[Calendar.DAY_OF_MONTH] == todayCalendar[Calendar.DAY_OF_MONTH]) 16 | val isYesterday = (tmpCalendar[Calendar.DAY_OF_MONTH] == (todayCalendar[Calendar.DAY_OF_MONTH]-1)) 17 | 18 | if (isSameYear and isSameMonth and isSameDay) { 19 | return getTodayDateFormat(tmpCalendar) 20 | } 21 | 22 | if(isSameYear and isSameMonth and isYesterday){ 23 | return getYesterdayDateFormat(tmpCalendar) 24 | } 25 | 26 | if(isSameYear and isSameMonth.not()){ 27 | return getSameYearDateFormat(tmpCalendar) 28 | } 29 | 30 | return getCompleteDateFormat(tmpCalendar) 31 | } 32 | 33 | // Today 5:00 PM 34 | abstract fun getTodayDateFormat(calendar: Calendar): String 35 | 36 | // Yesterday 5:00 PM 37 | abstract fun getYesterdayDateFormat(calendar: Calendar): String 38 | 39 | // Dec 31 40 | abstract fun getSameYearDateFormat(calendar: Calendar): String 41 | 42 | // 2020 Dec 1 43 | abstract fun getCompleteDateFormat(calendar: Calendar): String 44 | 45 | 46 | 47 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/formatter/ExpenseRecentDateFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.formatter 2 | 3 | import android.content.Context 4 | import com.arduia.expense.R 5 | import dagger.hilt.android.qualifiers.ApplicationContext 6 | import java.text.SimpleDateFormat 7 | import java.util.* 8 | import javax.inject.Inject 9 | 10 | class ExpenseRecentDateFormatter @Inject constructor(): ExpenseDateFormatter() { 11 | 12 | private val todayText = "Today" 13 | private val yesterdayText = "Yesterday" 14 | private val formatter = SimpleDateFormat(COMPLETE_DATE_PATTERN, Locale.ENGLISH) 15 | 16 | override fun getTodayDateFormat(calendar: Calendar): String { 17 | formatter.applyPattern(TIME_DATE_PATTERN) 18 | return todayText+" ${formatter.format(calendar.time)}" 19 | } 20 | 21 | override fun getSameYearDateFormat(calendar: Calendar): String { 22 | formatter.applyPattern(SAME_YEAR_DATE_PATTERN) 23 | return formatter.format(calendar.time) 24 | } 25 | 26 | override fun getCompleteDateFormat(calendar: Calendar): String { 27 | formatter.applyPattern(COMPLETE_DATE_PATTERN) 28 | return formatter.format(calendar.time) 29 | } 30 | 31 | override fun getYesterdayDateFormat(calendar: Calendar): String { 32 | formatter.applyPattern(TIME_DATE_PATTERN) 33 | return yesterdayText+" ${formatter.format(calendar.time)}" 34 | } 35 | 36 | companion object{ 37 | private const val TIME_DATE_PATTERN = "h:mm a" 38 | private const val SAME_YEAR_DATE_PATTERN = "MMM d" 39 | private const val COMPLETE_DATE_PATTERN = "yyyy MMM d" 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/formatter/MonthDateRangeFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.formatter 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.* 5 | import javax.inject.Inject 6 | 7 | class MonthDateRangeFormatter @Inject constructor() : StatisticDateRangeFormatter() { 8 | override val startFormatter: SimpleDateFormat = SimpleDateFormat("MMM d", Locale.ENGLISH) 9 | override fun getDateRangeDifferentYears(start: Long, end: Long): String { 10 | return getDateRangeSameYearDifferentMonth(start, end) 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/helper/MarginItemDecoration.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.helper 2 | 3 | import android.graphics.Rect 4 | import android.view.View 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class MarginItemDecoration( private val spaceHeight: Int = 0, 8 | private val spaceSide: Int? = null, 9 | private val isHorizontal: Boolean = false): RecyclerView.ItemDecoration(){ 10 | 11 | override fun getItemOffsets( 12 | outRect: Rect, 13 | view: View, 14 | parent: RecyclerView, 15 | state: RecyclerView.State 16 | ) { 17 | 18 | with(outRect){ 19 | // Fist tem should has top height 20 | if(parent.getChildAdapterPosition(view) == 0){ 21 | when(isHorizontal){ 22 | false -> top = spaceHeight 23 | } 24 | } 25 | 26 | bottom = spaceHeight 27 | 28 | spaceSide?.let { 29 | left = it 30 | right = it 31 | } 32 | } 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/language/LanguageProvider.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.language 2 | 3 | interface LanguageProvider { 4 | 5 | fun getLanguageVtoByID(id: String): LanguageUiModel 6 | 7 | fun getAvailableLanguages(): List 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/language/LanguageProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.language 2 | 3 | import com.arduia.expense.R 4 | import java.lang.Exception 5 | import javax.inject.Inject 6 | 7 | class LanguageProviderImpl @Inject constructor(): LanguageProvider { 8 | 9 | override fun getLanguageVtoByID(id: String): LanguageUiModel { 10 | return languageList.find { it.id == id} ?:throw Exception("Language id $id not found!") 11 | } 12 | 13 | override fun getAvailableLanguages() = languageList 14 | 15 | init { 16 | init() 17 | } 18 | 19 | private fun init(){ 20 | languageList = getAllLanguages() 21 | } 22 | 23 | private fun getAllLanguages() = mutableListOf().apply { 24 | add(LanguageUiModel("my", R.drawable.flag_myanmar, "Myanmar(Burma)")) 25 | add(LanguageUiModel("en", R.drawable.flag_united_states, "United States(English)")) 26 | add(LanguageUiModel("cn", R.drawable.flag_china, "China (Chinese)")) 27 | } 28 | 29 | companion object{ 30 | private var languageList = emptyList() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/language/LanguageUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.language 2 | 3 | import android.view.View 4 | import androidx.annotation.DrawableRes 5 | import com.arduia.expense.ui.onboarding.IntVisibility 6 | 7 | data class LanguageUiModel( 8 | val id: String, 9 | @DrawableRes val flag: Int, 10 | val name: String, 11 | @IntVisibility val isSelectedVisible: Int = View.INVISIBLE 12 | ) -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/mapper/CurrencyUiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.mapper 2 | 3 | import android.view.View 4 | import com.arduia.core.arch.Mapper 5 | import com.arduia.expense.data.local.CurrencyDto 6 | import com.arduia.expense.ui.onboarding.CurrencyUiModel 7 | import javax.inject.Inject 8 | 9 | class CurrencyUiModelMapper @Inject constructor(): Mapper{ 10 | override fun map(input: CurrencyDto): CurrencyUiModel { 11 | return CurrencyUiModel(input.name, input.symbol, input.number, View.INVISIBLE) 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/common/uimodel/DeleteInfoUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.common.uimodel 2 | 3 | data class DeleteInfoUiModel(val itemTotal: Int, val info: String?=null) 4 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/entry/ExpenseEntryMode.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.entry 2 | 3 | enum class ExpenseEntryMode{ 4 | INSERT, UPDATE 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/entry/ExpenseUpdateDataUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.entry 2 | 3 | import com.arduia.expense.ui.common.category.ExpenseCategory 4 | 5 | class ExpenseUpdateDataUiModel(val id: Int, 6 | val name: String, 7 | val date: Long, 8 | val category: ExpenseCategory, 9 | val amount: String, 10 | val note: String) 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/entry/ExpenseUpdateDataUiModelMapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.entry 2 | 3 | import com.arduia.core.arch.Mapper 4 | import com.arduia.expense.data.local.ExpenseEnt 5 | import com.arduia.expense.ui.common.category.ExpenseCategoryProvider 6 | import java.math.BigDecimal 7 | import java.text.DecimalFormat 8 | import java.text.NumberFormat 9 | import java.util.* 10 | import javax.inject.Inject 11 | 12 | class ExpenseUpdateDataUiModelMapper @Inject constructor( 13 | private val categoryProvider: ExpenseCategoryProvider 14 | ) : Mapper { 15 | 16 | private val decimalFormat = 17 | (NumberFormat.getNumberInstance(Locale.ENGLISH) as DecimalFormat).apply { 18 | isGroupingUsed = false 19 | } 20 | 21 | override fun map(input: ExpenseEnt) = ExpenseUpdateDataUiModel( 22 | id = input.expenseId, 23 | name = input.name ?: "", 24 | date = input.createdDate, 25 | amount = input.amount.getActual().updateFormat(), 26 | category = categoryProvider.getCategoryByID(input.category), 27 | note = input.note ?: "" 28 | ) 29 | 30 | private fun BigDecimal.updateFormat(): String { 31 | return decimalFormat.format(this) 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/entry/FloatingInputFilter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.entry 2 | 3 | import android.os.PatternMatcher 4 | import android.text.InputFilter 5 | import android.text.Spanned 6 | import timber.log.Timber 7 | import java.util.regex.Pattern 8 | 9 | class FloatingInputFilter : InputFilter { 10 | 11 | private val floatingPattern = Pattern.compile("[+-]?([0-9]{0,8}([.][0-9]{0,2})?|[.][0-9]{0,2})") 12 | // 123 13 | // 123. 14 | // 123.456 15 | // .456 16 | 17 | 18 | override fun filter( 19 | charSequence: CharSequence?, 20 | start: Int, 21 | end: Int, 22 | dest: Spanned?, 23 | dstart: Int, 24 | dend: Int 25 | ): CharSequence { 26 | 27 | if (charSequence == null) return "" 28 | if (dest == null) return "" 29 | 30 | val numberText = dest.toString().replaceRange(dstart, dend, charSequence) 31 | val isMatches = floatingPattern.matcher(numberText).matches() 32 | return if (isMatches) charSequence else "" 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/entry/LockMode.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.entry 2 | 3 | enum class LockMode { 4 | LOCKED, UNLOCK 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/ExpenseEntToLogVoMapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs 2 | 3 | import com.arduia.core.arch.Mapper 4 | import com.arduia.expense.data.local.ExpenseEnt 5 | import com.arduia.expense.ui.home.CurrencyProvider 6 | import javax.inject.Inject 7 | 8 | class ExpenseEntToLogVoMapper @Inject constructor(private val mapper: Mapper) : 9 | Mapper { 10 | override fun map(input: ExpenseEnt): ExpenseLogUiModel { 11 | return mapper.map(input) 12 | } 13 | 14 | class ExpenseEntToLogVoMapperFactoryImpl @Inject constructor(private val factory: ExpenseUiModelMapperFactory): 15 | ExpenseEntToLogVoMapperFactory { 16 | override fun create(currencyProvider: CurrencyProvider): Mapper { 17 | return ExpenseEntToLogVoMapper(factory.create(currencyProvider)) 18 | } 19 | } 20 | } 21 | 22 | interface ExpenseEntToLogVoMapperFactory: Mapper.Factory{ 23 | fun create(currencyProvider: CurrencyProvider): Mapper 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/ExpenseLogUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs 2 | 3 | sealed class ExpenseLogUiModel { 4 | 5 | class Header(val date: String) : ExpenseLogUiModel() 6 | 7 | class Log( 8 | val expenseLog: ExpenseUiModel, 9 | val headerPosition: Int 10 | ) : ExpenseLogUiModel() 11 | 12 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/ExpenseMode.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs 2 | 3 | enum class ExpenseMode { 4 | NORMAL,SELECTION 5 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/ExpenseUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs 2 | 3 | import androidx.annotation.DrawableRes 4 | 5 | data class ExpenseUiModel( 6 | val id: Int, 7 | val name: String, 8 | val date: String, 9 | @DrawableRes 10 | val category: Int, 11 | val amount: String, 12 | val currencySymbol: String, 13 | val finance: String 14 | ) 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/swipe/SwipeItemCallback.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs.swipe 2 | 3 | import android.graphics.Canvas 4 | import androidx.recyclerview.widget.ItemTouchHelper 5 | import androidx.recyclerview.widget.RecyclerView 6 | 7 | class SwipeItemCallback : 8 | ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { 9 | 10 | override fun onMove( 11 | recyclerView: RecyclerView, 12 | viewHolder: RecyclerView.ViewHolder, 13 | target: RecyclerView.ViewHolder 14 | ): Boolean = false 15 | 16 | override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { 17 | super.onSelectedChanged(viewHolder, actionState) 18 | if (viewHolder !is SwipeListenerVH) return 19 | viewHolder.onSwipeItemChanged() 20 | } 21 | 22 | override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {} 23 | 24 | override fun onChildDraw( 25 | c: Canvas, 26 | recyclerView: RecyclerView, 27 | viewHolder: RecyclerView.ViewHolder, 28 | dX: Float, 29 | dY: Float, 30 | actionState: Int, 31 | isCurrentlyActive: Boolean 32 | ) { 33 | if (viewHolder !is SwipeListenerVH) return 34 | viewHolder.onSwipe(isCurrentlyActive, dX) 35 | } 36 | 37 | override fun getSwipeEscapeVelocity(defaultValue: Float): Float { 38 | return Float.MAX_VALUE 39 | } 40 | 41 | override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float { 42 | return Float.MAX_VALUE 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/swipe/SwipeItemState.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs.swipe 2 | 3 | import androidx.annotation.IntDef 4 | 5 | class SwipeItemState{ 6 | 7 | companion object{ 8 | const val STATE_IDLE = -1 9 | const val STATE_LOCK_START = 1 10 | const val STATE_LOCK_END = 2 11 | } 12 | 13 | @IntDef(value =[STATE_IDLE, STATE_LOCK_START, STATE_LOCK_END]) 14 | @MustBeDocumented 15 | annotation class SwipeState 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/swipe/SwipeListenerVH.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs.swipe 2 | 3 | interface SwipeListenerVH { 4 | 5 | fun onSwipe(isOnTouch: Boolean, dx: Float) 6 | 7 | fun onSwipeItemChanged() 8 | 9 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/expenselogs/swipe/SwipeStateHolder.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.expenselogs.swipe 2 | 3 | import java.lang.Exception 4 | import java.util.HashMap 5 | 6 | class SwipeStateHolder{ 7 | 8 | private val stateList: HashMap = hashMapOf() 9 | 10 | fun updateState(id: Int, @SwipeItemState.SwipeState state: Int){ 11 | stateList[id] = state 12 | } 13 | 14 | fun getStateOrError(id: Int): Int{ 15 | return getStateOrNull(id) ?: throw Exception("id($id) not found") 16 | } 17 | 18 | fun removeState(id: Int){ 19 | stateList.remove(id) 20 | } 21 | 22 | fun getCount(@SwipeItemState.SwipeState state: Int): Int{ 23 | 24 | if(stateList.isEmpty()) return 0 25 | 26 | return stateList.count {item -> item.value == state} 27 | } 28 | 29 | fun getSelectIdList(): List{ 30 | 31 | if(stateList.isEmpty()) return emptyList() 32 | 33 | return stateList.filter { it.value == SwipeItemState.STATE_LOCK_START }.map { it.key } 34 | } 35 | 36 | fun clear(){ 37 | stateList.clear() 38 | } 39 | 40 | fun getStateOrNull(id: Int): Int? = stateList[id] 41 | 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/feedback/FeedbackStatusDialog.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.feedback 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.navigation.fragment.findNavController 8 | import com.arduia.expense.R 9 | import com.arduia.expense.databinding.FragFeedbackStatusDialogBinding 10 | import com.google.android.material.bottomsheet.BottomSheetDialogFragment 11 | 12 | class FeedbackStatusDialog : BottomSheetDialogFragment() { 13 | 14 | private var _binding: FragFeedbackStatusDialogBinding? = null 15 | private val binding get() = _binding!! 16 | 17 | override fun onCreateView( 18 | inflater: LayoutInflater, 19 | container: ViewGroup?, 20 | savedInstanceState: Bundle? 21 | ): View { 22 | _binding = FragFeedbackStatusDialogBinding.inflate(layoutInflater, container, false) 23 | return binding.root 24 | } 25 | 26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 27 | super.onViewCreated(view, savedInstanceState) 28 | setupView() 29 | } 30 | 31 | private fun setupView() { 32 | setupGoHomeButton() 33 | } 34 | 35 | private fun setupGoHomeButton() { 36 | binding.goHome.setOnClickListener { 37 | popBackHome() 38 | dismiss() 39 | } 40 | 41 | binding.btnDrop.setOnClickListener { 42 | dismiss() 43 | } 44 | } 45 | 46 | private fun popBackHome() { 47 | findNavController().popBackStack(R.id.dest_home, false) 48 | } 49 | 50 | override fun onDestroyView() { 51 | super.onDestroyView() 52 | _binding = null 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/home/ExpenseDayNameProvider.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.home 2 | 3 | import android.content.Context 4 | import androidx.annotation.StringRes 5 | import com.arduia.expense.R 6 | import com.arduia.graph.DayNameProvider 7 | import dagger.hilt.android.qualifiers.ActivityContext 8 | import dagger.hilt.android.qualifiers.ApplicationContext 9 | import java.lang.IllegalArgumentException 10 | import javax.inject.Inject 11 | 12 | class ExpenseDayNameProvider @Inject constructor(@ApplicationContext val context: Context): DayNameProvider { 13 | override fun getName(day: Int): String = 14 | when (day) { 15 | 1 -> getString(R.string.day_sun) 16 | 2 -> getString(R.string.day_mon) 17 | 3 -> getString(R.string.day_tue) 18 | 4 -> getString(R.string.day_wed) 19 | 5 -> getString(R.string.day_thu) 20 | 6 -> getString(R.string.day_fri) 21 | 7 -> getString(R.string.day_sat) 22 | else -> throw IllegalArgumentException("The day $day is not in the range of 1 to 7") 23 | } 24 | private fun getString(@StringRes id: Int) = context.getString(id) 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/home/ExpenseGraphAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.home 2 | 3 | import com.arduia.graph.SpendGraph 4 | 5 | class ExpenseGraphAdapter : SpendGraph.Adapter(){ 6 | 7 | var expenseMap = mapOf() 8 | set(value) { 9 | field = value 10 | notifyDataChanged() 11 | } 12 | 13 | override fun getRate(day: Int): Int { 14 | return expenseMap[day] ?: -1 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/home/ExpenseRateCalculator.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.home 2 | 3 | import com.arduia.expense.data.local.ExpenseEnt 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.flow.Flow 6 | 7 | interface ExpenseRateCalculator { 8 | 9 | fun getRates(): Flow> 10 | 11 | suspend fun setWeekExpenses(list: List) 12 | 13 | interface Factory{ 14 | fun create(scope: CoroutineScope): ExpenseRateCalculator 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/home/IncomeOutcomeEpoxyModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.home 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import com.airbnb.epoxy.* 6 | import com.arduia.expense.R 7 | import com.arduia.expense.databinding.LayoutExpenseInOutBinding 8 | 9 | data class IncomeOutcomeUiModel( 10 | val incomeValue: String, 11 | val outComeValue: String, 12 | val currencySymbol: String, 13 | val dateRange: String 14 | ) 15 | 16 | @SuppressLint("NonConstantResourceId") 17 | @EpoxyModelClass(layout = R.layout.layout_expense_in_out) 18 | abstract class IncomeOutcomeEpoxyModel : EpoxyModelWithHolder() { 19 | 20 | @EpoxyAttribute 21 | lateinit var data: IncomeOutcomeUiModel 22 | 23 | override fun bind(holder: VH) { 24 | with(holder.binding) { 25 | tvIncomeValue.text = data.incomeValue 26 | tvOutcomeValue.text = data.outComeValue 27 | tvOutcomeSymbol.text = data.currencySymbol 28 | tvIncomeSymobol.text = data.currencySymbol 29 | tvDateRange.text = data.dateRange 30 | } 31 | } 32 | 33 | inner class VH : EpoxyHolder() { 34 | lateinit var binding: LayoutExpenseInOutBinding 35 | override fun bindView(itemView: View) { 36 | binding = LayoutExpenseInOutBinding.bind(itemView) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/home/WeeklyGraphUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.home 2 | 3 | import android.annotation.SuppressLint 4 | import android.view.View 5 | import com.airbnb.epoxy.EpoxyAttribute 6 | import com.airbnb.epoxy.EpoxyHolder 7 | import com.airbnb.epoxy.EpoxyModelClass 8 | import com.airbnb.epoxy.EpoxyModelWithHolder 9 | import com.arduia.expense.R 10 | import com.arduia.expense.databinding.LayoutExpenseGraphBinding 11 | 12 | data class WeeklyGraphUiModel(val dateRange: String, val rate: Map) 13 | 14 | @SuppressLint("NonConstantResourceId") 15 | @EpoxyModelClass(layout = R.layout.layout_expense_graph) 16 | abstract class WeeklyGraphEpoxyModel : EpoxyModelWithHolder() { 17 | 18 | @EpoxyAttribute 19 | lateinit var data: WeeklyGraphUiModel 20 | 21 | inner class VH : EpoxyHolder() { 22 | lateinit var binding: LayoutExpenseGraphBinding 23 | lateinit var adapter: ExpenseGraphAdapter 24 | override fun bindView(itemView: View) { 25 | binding = LayoutExpenseGraphBinding.bind(itemView) 26 | adapter = ExpenseGraphAdapter() 27 | binding.expenseGraph.adapter = adapter 28 | } 29 | } 30 | 31 | override fun bind(holder: VH) { 32 | with(holder) { 33 | adapter.expenseMap = data.rate 34 | binding.tvDateRange.text = data.dateRange 35 | } 36 | } 37 | 38 | override fun unbind(holder: VH) { 39 | holder.binding.expenseGraph.adapter = null 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/onboarding/CurrencyUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.onboarding 2 | 3 | import android.view.View 4 | import androidx.annotation.IntDef 5 | import androidx.annotation.IntRange 6 | 7 | data class CurrencyUiModel( 8 | val name: String, 9 | val symbol: String, 10 | val number: String, 11 | @IntVisibility 12 | val isSelectionVisible: Int 13 | ) 14 | 15 | @IntDef(value = [View.VISIBLE,View.INVISIBLE]) 16 | annotation class IntVisibility -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/onboarding/OnBoardingConfigViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.onboarding 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.arduia.expense.data.SettingsRepository 6 | import com.arduia.mvvm.EventLiveData 7 | import com.arduia.mvvm.EventUnit 8 | import com.arduia.mvvm.post 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class OnBoardingConfigViewModel @Inject constructor(private val settingRepo: SettingsRepository) : 16 | ViewModel(){ 17 | 18 | private val _onRestart = EventLiveData() 19 | val onRestart get() = _onRestart.asLiveData() 20 | 21 | private val _onContinued = EventLiveData() 22 | val onContinued get() = _onContinued.asLiveData() 23 | 24 | fun finishedConfig(){ 25 | viewModelScope.launch(Dispatchers.IO){ 26 | settingRepo.setFirstUser(false) 27 | _onRestart post EventUnit 28 | } 29 | } 30 | 31 | fun continued(){ 32 | _onContinued post EventUnit 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/onboarding/OnBoardingStateAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.onboarding 2 | 3 | import androidx.fragment.app.Fragment 4 | import androidx.viewpager2.adapter.FragmentStateAdapter 5 | 6 | class OnBoardingStateAdapter(parent: Fragment): FragmentStateAdapter(parent){ 7 | override fun getItemCount(): Int { 8 | return 2 9 | } 10 | 11 | override fun createFragment(position: Int): Fragment { 12 | return when(position){ 13 | 0 -> ChooseLanguageFragment() 14 | else -> ChooseCurrencyFragment() 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/splash/SplashViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.splash 2 | 3 | 4 | import androidx.lifecycle.* 5 | import com.arduia.expense.data.CurrencyRepository 6 | import com.arduia.expense.data.SettingsRepository 7 | import com.arduia.expense.model.Result 8 | import com.arduia.expense.model.awaitValueOrError 9 | import com.arduia.mvvm.EventLiveData 10 | import com.arduia.mvvm.EventUnit 11 | import com.arduia.mvvm.post 12 | import dagger.hilt.android.lifecycle.HiltViewModel 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.flow.* 16 | import kotlinx.coroutines.launch 17 | import javax.inject.Inject 18 | 19 | @HiltViewModel 20 | class SplashViewModel @Inject 21 | constructor( 22 | private val settingsRepository: SettingsRepository 23 | ) : ViewModel() { 24 | 25 | private val _firstTimeEvent = EventLiveData() 26 | val firstTimeEvent = _firstTimeEvent.asLiveData() 27 | 28 | private val _normalUserEvent = EventLiveData() 29 | val normalUserEvent = _normalUserEvent.asLiveData() 30 | 31 | private val splashDuration = 1000L 32 | 33 | init { 34 | checkUserAndGo() 35 | } 36 | 37 | private fun checkUserAndGo() { 38 | viewModelScope.launch(Dispatchers.IO) { 39 | val isFirstTimeUser = settingsRepository.getFirstUser().awaitValueOrError() 40 | delay(splashDuration) 41 | if (isFirstTimeUser) { 42 | _firstTimeEvent post EventUnit 43 | } else { 44 | _normalUserEvent post EventUnit 45 | } 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/statistics/CategoryAnalyzer.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.statistics 2 | 3 | import com.arduia.expense.data.local.ExpenseEnt 4 | 5 | interface CategoryAnalyzer { 6 | fun analyze(entities: List): List 7 | } -------------------------------------------------------------------------------- /app/src/main/java/com/arduia/expense/ui/statistics/CategoryStatisticUiModel.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.ui.statistics 2 | 3 | data class CategoryStatisticUiModel(val name: String, val progress: Float, val progressText: String) -------------------------------------------------------------------------------- /app/src/main/res/anim/expense_enter_left.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/anim/expense_exit_right.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/anim/pop_down_up.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/pop_up_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/color/category_background_color_statelist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/color/navigation_menu_text_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_large_rounded_warning.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bg_small_rounded_warning.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/flag_germany.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/flag_myanmar.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_accomplish.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_arrow_start.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_back.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_backup.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_borrow.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_check.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_checked.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_close.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_clothes.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_date.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_delete.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_done_circle.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_down_arrow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_drop_down.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_edit.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_education.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_entertainment.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_expense_logs.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_export.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_feedback.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_filter.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_folder.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_food.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_history.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_home.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_import.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_income.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_info.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background_rounded.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground_rounded.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_like.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lock_closed.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_lock_open.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_loop.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_menu.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_minus.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_horizontal.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_more_next.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_nav_next.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_outcome.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_report.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_save.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_search.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_select_all.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_settings.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_social.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_statistics.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_theme.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_time.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_transportation.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_up_arrrow.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_update.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/font/poppins.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/font/poppins_medium.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/pyidaungsu.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/font/pyidaungsu_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/font/pyidaungsu_bold.ttf -------------------------------------------------------------------------------- /app/src/main/res/font/pyidaungsu_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/font/pyidaungsu_regular.ttf -------------------------------------------------------------------------------- /app/src/main/res/layout/choose_theme_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | 17 | 24 | 25 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/frag_about_update_dialog.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 26 | 27 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/res/layout/frag_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 16 | 17 | 24 | 25 | 26 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/layout/frag_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_category.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/layout/item_expense_date_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_search_box.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 22 | 23 | 32 | 33 | -------------------------------------------------------------------------------- /app/src/main/res/layout/layout_toolbar.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_expense_log.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values-my/font_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 12 | 13 | 17 | 18 | 21 | 22 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 25 | 26 | @color/dark_gray 27 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/values/attr.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #81d4fa 4 | #039be5 5 | #4fc3f7 6 | #0288d1 7 | #b3e5fc 8 | #ffeb3b 9 | #fff176 10 | #ef5350 11 | #e57373 12 | #e0e0e0 13 | #eeeeee 14 | #f5f5f5 15 | #ffffff 16 | #000000 17 | #212121 18 | #81c784 19 | #66bb6a 20 | #4caf50 21 | @android:color/darker_gray 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4dp 4 | 8dp 5 | 16dp 6 | 32dp 7 | 8 | 2dp 9 | 56dp 10 | 11 | 450dp 12 | 482dp 13 | 10sp 14 | 20dp 15 | 35dp 16 | 20dp 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values/font_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 13 | 14 | 17 | 18 | 21 | 22 | 26 | 27 | -------------------------------------------------------------------------------- /app/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 250 4 | 400 5 | 400 6 | 350 7 | 1000 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/theme.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 22 | 26 | 27 | @color/gray_100 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/test/java/com/arduia/expense/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backup/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /backup/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion rootProject.compileSdk 6 | buildToolsVersion rootProject.buildToolVersion 7 | 8 | defaultConfig { 9 | minSdkVersion rootProject.minSdk 10 | targetSdkVersion rootProject.targetSdk 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: "libs", include: ["*.jar"]) 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 29 | implementation 'androidx.core:core-ktx:1.3.2' 30 | implementation 'androidx.appcompat:appcompat:1.2.0' 31 | testImplementation 'junit:junit:4.13.1' 32 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 34 | 35 | 36 | def jxl_version = "2.6.12" 37 | implementation "net.sourceforge.jexcelapi:jxl:$jxl_version" 38 | } 39 | -------------------------------------------------------------------------------- /backup/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/backup/consumer-rules.pro -------------------------------------------------------------------------------- /backup/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /backup/src/androidTest/java/com/arduia/backup/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.arduia.backup.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backup/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/AbstractBackupSheet.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | import com.arduia.backup.task.BackupResult 4 | 5 | internal interface AbstractBackupSheet { 6 | 7 | suspend fun import(input: I): BackupResult 8 | 9 | suspend fun export(out: O, index: Int): BackupResult 10 | 11 | suspend fun getItemCount(input: I): Int 12 | } -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/BackupException.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | class BackupException(message: String = "", cause: Throwable? = null) : Exception(message, cause) 4 | -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/BackupSource.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | interface BackupSource { 4 | 5 | suspend fun write(item: T) 6 | 7 | suspend fun writeAll(items: List) 8 | 9 | suspend fun readAll(): List 10 | 11 | suspend fun totalCountAll(): Int 12 | 13 | } 14 | -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/FileNameGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | interface FileNameGenerator: NameGenerator -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/NameGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | interface NameGenerator { 4 | fun generate(): String 5 | } -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/SheetFieldInfo.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | import java.util.LinkedHashMap 4 | 5 | class SheetFieldInfo private constructor(): LinkedHashMap() { 6 | companion object { 7 | fun createFromMap(map: Map): SheetFieldInfo { 8 | return SheetFieldInfo().apply { 9 | putAll(map) 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/SheetRow.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | import java.util.LinkedHashMap 4 | 5 | 6 | class SheetRow private constructor(): LinkedHashMap(){ 7 | companion object{ 8 | fun createFromMap(map: Map): SheetRow { 9 | return SheetRow().apply { 10 | putAll(map) 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/generator/BackupNameGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup.generator 2 | 3 | import com.arduia.backup.FileNameGenerator 4 | import java.text.DateFormat 5 | import java.text.SimpleDateFormat 6 | import java.util.* 7 | 8 | class BackupNameGenerator (private val dateFormatter: DateFormat = SimpleDateFormat(DATE_PATTERN, Locale.ENGLISH)): 9 | FileNameGenerator { 10 | 11 | companion object{ 12 | private const val PREFIX = "Backup" 13 | private const val DATE_PATTERN = "_MMMd_yyyy_Hms" 14 | } 15 | 16 | override fun generate(): String { 17 | val date = Date().time 18 | val dateSuffix = dateFormatter.format(date) 19 | return "$PREFIX$dateSuffix" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/task/BackupResult.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup.task 2 | 3 | import java.lang.Exception 4 | 5 | interface BackupResult { 6 | val data: T? 7 | val exception: Exception 8 | operator fun plus(value: BackupResult): BackupResult 9 | } 10 | 11 | internal class BackupCountResult(override val data: Int?) : BackupResult { 12 | override val exception: Exception 13 | get() = if (data != null) EmptyException() else Exception("Result Not Exit!") 14 | 15 | override fun plus(value: BackupResult): BackupResult { 16 | val plusData = value.data ?: return this 17 | val currentData = data ?: return BackupCountResult(plusData) 18 | return BackupCountResult(plusData + currentData) 19 | } 20 | 21 | companion object { 22 | fun empty(): BackupResult = BackupCountResult(data = null) 23 | } 24 | } 25 | 26 | internal class EmptyException() : Exception() 27 | 28 | fun BackupResult.getDataOrError(): T = this.data ?: throw exception -------------------------------------------------------------------------------- /backup/src/main/java/com/arduia/backup/task/BackupTask.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup.task 2 | 3 | interface BackupTask { 4 | val id: Int 5 | val name: String? 6 | val result: BackupResult 7 | } -------------------------------------------------------------------------------- /backup/src/test/java/com/arduia/backup/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.backup 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | ext.kotlin_version = "1.6.21" 4 | ext.targetSdk = 33 5 | ext.compileSdk = 33 6 | ext.minSdk = 21 7 | ext.buildToolVersion ="29.0.3" 8 | repositories { 9 | 10 | mavenCentral() 11 | google() 12 | jcenter() 13 | } 14 | 15 | dependencies { 16 | 17 | classpath "com.android.tools.build:gradle:4.2.0" 18 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 19 | classpath 'com.google.dagger:hilt-android-gradle-plugin:2.42' 20 | def nav_version = "2.3.0" 21 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" 22 | 23 | // NOTE: Do not place your application dependencies here; they belong 24 | // in the individual module build.gradle files 25 | } 26 | } 27 | 28 | allprojects { 29 | repositories { 30 | google() 31 | jcenter() 32 | maven{ 33 | url 'https://jitpack.io' 34 | } 35 | mavenCentral() 36 | } 37 | } 38 | 39 | task clean(type: Delete) { 40 | delete rootProject.buildDir 41 | } 42 | -------------------------------------------------------------------------------- /currency-store/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /currency-store/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java-library' 3 | id 'kotlin' 4 | } 5 | 6 | java { 7 | sourceCompatibility = JavaVersion.VERSION_1_7 8 | targetCompatibility = JavaVersion.VERSION_1_7 9 | } 10 | 11 | dependencies { 12 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 13 | } -------------------------------------------------------------------------------- /currency-store/src/main/java/com/arduia/currencystore/Rate.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.currencystore 2 | 3 | /** 4 | * Expense Store Conversions for every Amount 5 | * to improve performance in calculations 6 | */ 7 | interface Rate { 8 | fun getRate(): I 9 | } -------------------------------------------------------------------------------- /currency-store/src/main/java/com/arduia/currencystore/Store.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.currencystore 2 | 3 | /** 4 | * To exchange between store currency value and actual currency value 5 | * Use in decimal value accuracy to convert after an expense amount is stored in database 6 | */ 7 | abstract class Store(val rate: Rate) { 8 | 9 | abstract fun getActual(): A 10 | 11 | abstract fun getStore(): S 12 | 13 | abstract fun setStore(value: S) 14 | 15 | } -------------------------------------------------------------------------------- /expense-backup/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /expense-backup/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'kotlin-android' 4 | } 5 | 6 | android { 7 | compileSdkVersion rootProject.compileSdk 8 | buildToolsVersion rootProject.buildToolVersion 9 | 10 | defaultConfig { 11 | minSdkVersion rootProject.minSdk 12 | targetSdkVersion rootProject.targetSdk 13 | versionCode 1 14 | versionName "1.0" 15 | 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | } 19 | 20 | buildTypes { 21 | release { 22 | minifyEnabled false 23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 24 | } 25 | } 26 | compileOptions { 27 | sourceCompatibility JavaVersion.VERSION_1_8 28 | targetCompatibility JavaVersion.VERSION_1_8 29 | } 30 | kotlinOptions { 31 | jvmTarget = '1.8' 32 | } 33 | } 34 | 35 | dependencies { 36 | 37 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 38 | implementation 'androidx.core:core-ktx:1.3.2' 39 | implementation 'androidx.appcompat:appcompat:1.2.0' 40 | implementation 'com.google.android.material:material:1.2.1' 41 | testImplementation 'junit:junit:4.+' 42 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 43 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 44 | 45 | def gson_version = "2.8.6" 46 | implementation "com.google.code.gson:gson:$gson_version" 47 | } -------------------------------------------------------------------------------- /expense-backup/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/expense-backup/consumer-rules.pro -------------------------------------------------------------------------------- /expense-backup/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /expense-backup/src/androidTest/java/com/arduia/expense/backup/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.arduia.expense.backup.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /expense-backup/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /expense-backup/src/main/java/com/arduia/expense/backup/MainCategoryField.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup 2 | 3 | data class MainCategoryField( 4 | val id: Int, 5 | val name: String 6 | ) { 7 | companion object { 8 | const val FILED_ID = "ID" 9 | const val FIELD_NAME = "NAME" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /expense-backup/src/main/java/com/arduia/expense/backup/Metadata.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup 2 | 3 | /** 4 | * Backup Metadata for current Version. 5 | */ 6 | class Metadata { 7 | companion object { 8 | const val VERSION_CODE = 1 9 | } 10 | } -------------------------------------------------------------------------------- /expense-backup/src/main/java/com/arduia/expense/backup/schema/BackupSchema.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup.schema 2 | 3 | import com.arduia.expense.backup.Metadata 4 | import com.arduia.expense.backup.schema.table.Table 5 | import com.google.gson.annotations.SerializedName 6 | import java.util.* 7 | 8 | /** 9 | * Backup Schema for every backup logs 10 | * to get backward compatibility for future versions 11 | */ 12 | data class BackupSchema( 13 | 14 | @SerializedName("version") 15 | val version: Int = Metadata.VERSION_CODE, 16 | 17 | @SerializedName("app_version_code") 18 | val appVersionCode: Long, 19 | 20 | @SerializedName("export_date") 21 | val exportDate: Long = Date().time, 22 | 23 | @SerializedName("currency_code") 24 | val currencyCode: String, 25 | 26 | @SerializedName("export_tables") 27 | val exportTables: List 28 | 29 | ) 30 | -------------------------------------------------------------------------------- /expense-backup/src/main/java/com/arduia/expense/backup/schema/table/Field.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup.schema.table 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * Field info for every column in each expense backup tables 7 | */ 8 | data class Field( 9 | 10 | @SerializedName("name") 11 | val name: String, 12 | 13 | @SerializedName("type") 14 | val type: String 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /expense-backup/src/main/java/com/arduia/expense/backup/schema/table/Table.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup.schema.table 2 | 3 | import com.google.gson.annotations.SerializedName 4 | 5 | /** 6 | * Backup Table Info 7 | */ 8 | data class Table( 9 | 10 | @SerializedName("names") 11 | val tableNames: List, 12 | 13 | @SerializedName("fields") 14 | val fields: List 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /expense-backup/src/test/java/com/arduia/expense/backup/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.expense.backup 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/2.txt: -------------------------------------------------------------------------------- 1 | First Release! 2 | 3 | v0.1.1-Beta 4 | 5 | - Expense Entry -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/3.txt: -------------------------------------------------------------------------------- 1 | v0.1.2-beta 2 | 3 | - Backup Expenses 4 | - Feedback 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/4.txt: -------------------------------------------------------------------------------- 1 | v0.1.2-beta2 2 | 3 | fix Bugs: 4 | - MM font error 5 | - Expense Lists Limit 6 | 7 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/5.txt: -------------------------------------------------------------------------------- 1 | v0.1.2-beta3 2 | 3 | Fix Bugs: 4 | - Backup update error 5 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/6.txt: -------------------------------------------------------------------------------- 1 | Bug Fixed. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/7.txt: -------------------------------------------------------------------------------- 1 | Bugs Fixed. 2 | 3 | New Features: 4 | - Repeat Expense Entry 5 | - Light/Dark Mode 6 | - Decimal Amount 7 | - Different Currencies 8 | - Category Statistics 9 | and more. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/changelogs/8.txt: -------------------------------------------------------------------------------- 1 | -Some Bugs Fixed. 2 | -UI improvements. 3 | -Chinese Language is available now! 4 | -Export Backup Log with Metadata -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/full_description.txt: -------------------------------------------------------------------------------- 1 | Key Features: 2 | 3 | Offline Storage 4 | Records are stored in local device storage. 5 | 6 | Statistics 7 | Weekly expenses are presented as simple Graph. 8 | Category Statistics 9 | 10 | Multi-Language support 11 | -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/featureGraphic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/featureGraphic.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/icon.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/images/phoneScreenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/short_description.txt: -------------------------------------------------------------------------------- 1 | A simple free finance note to safely record daily expense without any Ads. -------------------------------------------------------------------------------- /fastlane/metadata/android/en-US/title.txt: -------------------------------------------------------------------------------- 1 | Pro Expense - Daily Finance Tracker -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | kapt.incremental.apt=true 23 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionSha256Sum=de8f52ad49bdc759164f72439a3bf56ddb1589c4cde802d3cec7d6ad0e0ee410 -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':expense-backup' 2 | include ':currency-store' 3 | include ':backup' 4 | include ':shared' 5 | include ':week-expense-graph' 6 | include ':app' 7 | rootProject.name = "ProExpense" 8 | -------------------------------------------------------------------------------- /shared/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /shared/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion rootProject.compileSdk 6 | buildToolsVersion rootProject.buildToolVersion 7 | 8 | defaultConfig { 9 | minSdkVersion rootProject.minSdk 10 | targetSdkVersion rootProject.targetSdk 11 | versionCode 1 12 | versionName "1.0" 13 | 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | consumerProguardFiles "consumer-rules.pro" 16 | } 17 | 18 | buildTypes { 19 | release { 20 | minifyEnabled false 21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 22 | } 23 | } 24 | } 25 | 26 | dependencies { 27 | implementation fileTree(dir: "libs", include: ["*.jar"]) 28 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 29 | implementation 'androidx.core:core-ktx:1.3.2' 30 | implementation 'androidx.appcompat:appcompat:1.2.0' 31 | testImplementation 'junit:junit:4.13.1' 32 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 33 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 34 | 35 | } 36 | -------------------------------------------------------------------------------- /shared/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/shared/consumer-rules.pro -------------------------------------------------------------------------------- /shared/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /shared/src/androidTest/java/com/arduia/core/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.arduia.core.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/arch/Mapper.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.arch 2 | 3 | /** 4 | * Architecture Component 5 | * 6 | * Base Mapper Type for every mapping class 7 | */ 8 | interface Mapper { 9 | 10 | fun map(input: I): O 11 | 12 | interface Builder 13 | 14 | interface Factory 15 | } -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/content/Context.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.content 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | 6 | /** 7 | * Current Application Code 8 | * Current Application Version 9 | */ 10 | fun Context.getApplicationVersionCode(): Long { 11 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 12 | packageManager.getPackageInfo(packageName, 0).longVersionCode 13 | } else packageManager.getPackageInfo(packageName, 0).versionCode.toLong() 14 | } 15 | 16 | fun Context.getApplicationVersionName(): String{ 17 | return packageManager.getPackageInfo(packageName, 0).versionName 18 | } -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/extension/Dimen.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.extension 2 | 3 | import android.content.Context 4 | import android.view.View 5 | 6 | /** 7 | * Dimension Converters from Pixel to Density Pixel or vice-visa. 8 | */ 9 | fun Context.dp(px: Float) = (px/resources.displayMetrics.density) 10 | 11 | fun Context.px(dp: Float) = (dp* resources.displayMetrics.density) 12 | 13 | fun Context.pxS(sp: Float) = (sp * resources.displayMetrics.scaledDensity) 14 | 15 | fun Context.sp(px: Float) = (px/resources.displayMetrics.scaledDensity) 16 | 17 | fun Context.dp(px: Int):Int = dp(px.toFloat()).toInt() 18 | 19 | fun Context.px(dp: Int):Int = px(dp.toFloat()).toInt() 20 | 21 | fun View.dp(px: Int) = context.dp(px) 22 | 23 | fun View.px(dp: Int) = context.px(dp) 24 | 25 | fun View.dp(px: Float) = context.dp(px) 26 | 27 | fun View.px(dp: Float) = context.px(dp) 28 | 29 | fun View.pxS(sp: Float) = context.pxS(sp) 30 | 31 | 32 | -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/extension/Drawable.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.extension 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.graphics.drawable.Drawable 6 | import androidx.annotation.ColorInt 7 | import androidx.annotation.DrawableRes 8 | import androidx.core.content.ContextCompat 9 | import androidx.core.graphics.drawable.DrawableCompat 10 | 11 | /** 12 | * Wrap Custom Tint color on the drawable @id 13 | */ 14 | fun Context.getDrawable(@DrawableRes id: Int,@ColorInt color: Int = Color.BLACK): Drawable{ 15 | 16 | val icon = ContextCompat.getDrawable(this, id) 17 | 18 | DrawableCompat.setTint(icon!!, color) 19 | 20 | return icon 21 | } 22 | -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/lang/LocaleUpdate.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.lang 2 | 3 | import android.content.Context 4 | import android.content.res.Configuration 5 | import java.util.* 6 | 7 | /** 8 | * To change locale of context - language changes 9 | */ 10 | fun Context.updateResource(language: String):Context{ 11 | val locale = Locale(language) 12 | Locale.setDefault(locale) 13 | val config = Configuration(resources.configuration) 14 | config.setLocale(locale) 15 | return createConfigurationContext(config) 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/performance/Duration.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.performance 2 | 3 | import android.util.Log.d 4 | import kotlin.system.measureNanoTime 5 | import kotlin.system.measureTimeMillis 6 | 7 | /** 8 | * To Measure Duration of A function or statements 9 | */ 10 | 11 | fun printDurationNano(tag:String, suffix:String = "", execution:() -> R): R{ 12 | 13 | var result:R? = null 14 | 15 | val duration = measureNanoTime { 16 | result = execution() 17 | } 18 | 19 | d(tag,"$suffix Duration is $duration ns") 20 | 21 | return result ?: throw Exception("No Result in execution Lambda") 22 | } 23 | 24 | fun printDurationMilli(tag:String, suffix:String = "", execution:() -> R):R{ 25 | 26 | var result:R? = null 27 | 28 | val duration = measureTimeMillis { 29 | result = execution() 30 | } 31 | 32 | d(tag,"$suffix Duration is $duration ms") 33 | 34 | return result ?: throw Exception("No Result in execution Lambda") 35 | } -------------------------------------------------------------------------------- /shared/src/main/java/com/arduia/core/view/View.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core.view 2 | 3 | import android.view.View 4 | 5 | 6 | fun View.asVisible() { 7 | visibility = View.VISIBLE 8 | } 9 | 10 | fun View.asInvisible(){ 11 | visibility = View.INVISIBLE 12 | } 13 | 14 | fun View.asGone(){ 15 | visibility = View.GONE 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/test/java/com/arduia/core/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.core 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /week-expense-graph/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /week-expense-graph/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | sourceCompatibility = JavaVersion.VERSION_1_7 6 | targetCompatibility = JavaVersion.VERSION_1_7 7 | 8 | android { 9 | compileSdkVersion rootProject.compileSdk 10 | buildToolsVersion rootProject.buildToolVersion 11 | 12 | defaultConfig { 13 | minSdkVersion rootProject.minSdk 14 | targetSdkVersion rootProject.targetSdk 15 | versionCode 1 16 | versionName "1.0" 17 | 18 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 19 | consumerProguardFiles "consumer-rules.pro" 20 | } 21 | 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | 29 | 30 | } 31 | 32 | dependencies { 33 | implementation fileTree(dir: "libs", include: ["*.jar"]) 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 35 | implementation 'androidx.core:core-ktx:1.3.2' 36 | implementation 'androidx.appcompat:appcompat:1.2.0' 37 | implementation project(path: ':shared') 38 | testImplementation 'junit:junit:4.13.1' 39 | androidTestImplementation 'androidx.test.ext:junit:1.1.2' 40 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' 41 | 42 | } 43 | -------------------------------------------------------------------------------- /week-expense-graph/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduia/ProExpense/334afae6d42647adffcccb361c3cb2159a82225b/week-expense-graph/consumer-rules.pro -------------------------------------------------------------------------------- /week-expense-graph/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /week-expense-graph/src/androidTest/java/com/arduia/graph/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.graph 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.arduia.graph.test", appContext.packageName) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /week-expense-graph/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /week-expense-graph/src/main/java/com/arduia/graph/DayNameProvider.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.graph 2 | 3 | /** 4 | * Provide Day Name String for Graph 5 | * 6 | * To Support Different Language 7 | */ 8 | interface DayNameProvider { 9 | 10 | fun getName(day: Int): String 11 | 12 | } 13 | -------------------------------------------------------------------------------- /week-expense-graph/src/main/java/com/arduia/graph/DefaultDayNameProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.graph 2 | 3 | import android.content.Context 4 | import androidx.annotation.ColorInt 5 | import java.lang.IllegalArgumentException 6 | 7 | internal class DefaultDayNameProviderImpl : DayNameProvider { 8 | 9 | override fun getName(day: Int): String { 10 | validateDayOfWeek(day) 11 | return when (day) { 12 | 1 -> "SU" 13 | 2 -> "MO" 14 | 3 -> "TU" 15 | 4 -> "WE" 16 | 5 -> "TH" 17 | 6 -> "FR" 18 | 7 -> "SA" 19 | else -> throw IllegalArgumentException("The day $day is not in the range of 1 to 7") // Should already throw 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /week-expense-graph/src/main/java/com/arduia/graph/SpendPoint.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.graph 2 | 3 | import java.lang.IllegalArgumentException 4 | 5 | 6 | internal data class SpendPoint(val day: Int, val rate: Float) { 7 | init { 8 | validateDayOfWeek(day) 9 | validateSpendPointRate(rate) 10 | } 11 | } 12 | 13 | internal fun validateDayOfWeek(day: Int) { 14 | if (day !in 1..7) throw IllegalArgumentException("Day($day) must be between from 1 to 7") 15 | } 16 | 17 | internal fun validateSpendPointRate(rate: Float) { 18 | if (rate !in -1f..1f) throw IllegalArgumentException("Rate($rate) must be between from -1f to 1f") 19 | } -------------------------------------------------------------------------------- /week-expense-graph/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /week-expense-graph/src/test/java/com/arduia/graph/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package com.arduia.graph 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } 18 | --------------------------------------------------------------------------------