diff --git a/.gitignore b/.gitignore index 603b140..1249651 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,5 @@ *.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx +.gradle/ +.idea/ +build/ +local.properties diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index 88ea3aa..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - - - - -
- - - - xmlns:android - - ^$ - - - -
-
- - - - xmlns:.* - - ^$ - - - BY_NAME - -
-
- - - - .*:id - - http://schemas.android.com/apk/res/android - - - -
-
- - - - .*:name - - http://schemas.android.com/apk/res/android - - - -
-
- - - - name - - ^$ - - - -
-
- - - - style - - ^$ - - - -
-
- - - - .* - - ^$ - - - BY_NAME - -
-
- - - - .* - - http://schemas.android.com/apk/res/android - - - ANDROID_ATTRIBUTE_ORDER - -
-
- - - - .* - - .* - - - BY_NAME - -
-
-
-
- - -
-
\ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index 79ee123..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 3e3960b..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 4adab6b..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - -<<<<<<< HEAD - - - - - 1.8 - - - - - - - -======= ->>>>>>> 6164b7ab9283971583ecc0a415a805140fc8b571 - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 7f68460..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..5456a21 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,55 @@ +# General +language: android +jdk: oraclejdk8 +dist: xenial +os: linux + +# Environment variables +env: + global: + - ANDROID_API_LEVEL=29 + - ANDROID_BUILD_TOOLS_VERSION=29.0.3 + +# Android +android: + components: + - tools + - platform-tools + - build-tools-$ANDROID_BUILD_TOOLS_VERSION + - android-$ANDROID_API_LEVEL + - extra-google-google_play_services + - extra-google-m2repository + - extra-android-m2repository + - addon-google_apis-google-$ANDROID_API_LEVEL + licenses: + - 'android-sdk-preview-license-.+' + - 'android-sdk-license-.+' + - 'google-gdk-license-.+' + +# Caching +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + - $HOME/.android/build-cache + +# Make gradle executable +before_script: + - chmod +x gradlew + +# Stages +stages: + - compile + - test + +# Jobs +jobs: + include: + - stage: compile + script: ./gradlew assembleDebug + - stage: test + script: ./gradlew test + - script: ./gradlew ktlintReleaseFormat \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 4b7fc37..c5d93ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,17 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlin-kapt' +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'androidx.navigation.safeargs' android { compileSdkVersion 29 buildToolsVersion "29.0.3" defaultConfig { - applicationId "com.example.easybill" - minSdkVersion 28 + applicationId "com.easybill" + minSdkVersion 29 targetSdkVersion 29 versionCode 1 versionName "1.0" @@ -23,15 +26,51 @@ android { } } + dataBinding { + enabled = true + } + } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation 'androidx.core:core-ktx:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation 'com.google.android.material:material:1.1.0' + + // Room + def room_version = "2.2.5" + implementation "androidx.room:room-runtime:$room_version" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + kapt "androidx.room:room-compiler:$room_version" + + // ViewModel + implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' + + // Coroutines + def coroutine_version = '1.1.0' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutine_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutine_version" + + // Navigation + implementation "android.arch.navigation:navigation-fragment-ktx:$navigationVersion" + implementation "android.arch.navigation:navigation-ui-ktx:$navigationVersion" + implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0' + + // Timber + implementation 'com.jakewharton.timber:timber:4.7.1' + + // Recyclerview + implementation "androidx.recyclerview:recyclerview:1.1.0" } + +ktlint { + android.set(true) + outputColorName.set("RED") +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/example/easybill/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/easybill/ExampleInstrumentedTest.kt similarity index 95% rename from app/src/androidTest/java/com/example/easybill/ExampleInstrumentedTest.kt rename to app/src/androidTest/java/com/easybill/ExampleInstrumentedTest.kt index 84505f5..feeeec3 100644 --- a/app/src/androidTest/java/com/example/easybill/ExampleInstrumentedTest.kt +++ b/app/src/androidTest/java/com/easybill/ExampleInstrumentedTest.kt @@ -1,4 +1,4 @@ -package com.example.easybill +package com.easybill import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/app/src/androidTest/java/com/easybill/database/BillTest.kt b/app/src/androidTest/java/com/easybill/database/BillTest.kt new file mode 100644 index 0000000..44805c4 --- /dev/null +++ b/app/src/androidTest/java/com/easybill/database/BillTest.kt @@ -0,0 +1,107 @@ +package com.easybill.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.easybill.database.dao.HeadDao +import com.easybill.database.model.Head +import org.hamcrest.CoreMatchers.* +import org.junit.After +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class BillTest { + private lateinit var headDao: HeadDao + private lateinit var db: EasyBillDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, EasyBillDatabase::class.java).build() + headDao = db.getHeadDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun insertAndGetTenBills() { + val numBills = 10 + + for (i in 1..numBills) { + // create a new head with test-data + val bill = Head() + bill.address = "TestAddress#$i" + bill.storeName = "StoreName#$i" + bill.salesTax = i.toDouble() + + // insert the bill and get it + bill.id = headDao.insert(bill) + val actual = headDao.getById(bill.id) + + // verify results + assertThat(actual.address, equalTo(bill.address)) + assertThat(actual.storeName, equalTo(bill.storeName)) + assertThat(actual.salesTax, equalTo(bill.salesTax)) + } + + // get all bills + val bs = headDao.getAll() + assertThat(bs.size, equalTo(numBills)) + } + + @Test + @Throws(Exception::class) + fun insertAndUpdateBill() { + // create a new bill with test-data + val bill = Head() + bill.address = "TestAddress" + bill.storeName = "StoreName" + bill.salesTax = 19.0 + + // insert the bill + bill.id = headDao.insert(bill) + + // now change the bill and update it + bill.address = "NewTestAddress" + bill.storeName = "NewStoreName" + bill.salesTax = 20.0 + headDao.update(bill) + + // get the updated bill and verify + val actual = headDao.getById(bill.id) + assertThat(actual.address, equalTo(bill.address)) + assertThat(actual.storeName, equalTo(bill.storeName)) + assertThat(actual.salesTax, equalTo(bill.salesTax)) + } + + @Test + @Throws(Exception::class) + fun insertAndDeleteBill() { + // create a new bill with test-data + val bill = Head() + bill.address = "TestAddress" + bill.storeName = "StoreName" + bill.salesTax = 19.0 + + // insert the bill + bill.id = headDao.insert(bill) + + // delete the bill + headDao.delete(bill) + + // the bill should now be deleted and null must be returned + val actual = headDao.getById(bill.id) + assertThat(actual, `is`(nullValue())) + } +} diff --git a/app/src/androidTest/java/com/easybill/database/BillWithItemsTest.kt b/app/src/androidTest/java/com/easybill/database/BillWithItemsTest.kt new file mode 100644 index 0000000..a9f621e --- /dev/null +++ b/app/src/androidTest/java/com/easybill/database/BillWithItemsTest.kt @@ -0,0 +1,120 @@ +package com.easybill.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.easybill.database.dao.HeadDao +import com.easybill.database.dao.ItemDao +import com.easybill.database.model.Item +import com.easybill.database.model.Head +import org.hamcrest.CoreMatchers.equalTo +import org.junit.After +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class BillWithItemsTest { + private lateinit var itemDao: ItemDao + private lateinit var headDao: HeadDao + private lateinit var db: EasyBillDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, EasyBillDatabase::class.java).build() + headDao = db.getHeadDao() + itemDao = db.getItemDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + + @Test + @Throws(Exception::class) + fun insertBillWithItemsGetBillWithItems() { + val numItems = 10 + + // create a new bill with test-data + val bill = Head() + bill.address = "TestAddress" + bill.storeName = "StoreName" + bill.salesTax = 19.0 + + // insert the bill + bill.id = headDao.insert(bill) + + // insert numItems items + for (i in 1..numItems) { + + // new item with bill id of previously inserted bill + val item = Item() + item.billId = bill.id + item.amount = i.toDouble() + item.name = "TestItem#$i" + item.nettoPrice = i.toDouble() + + // insert item + item.id = itemDao.insert(item) + } + + // get bill with items + val actual = headDao.getBillById(bill.id) + + // bill should have numItems items + assertThat(actual.items.size, equalTo(numItems)) + } + + @Test + @Throws(Exception::class) + fun insertBillWithItemsDeleteItemsAndGetBillWithItems() { + val numItems = 10 + + // create a new bill with test-data + val bill = Head() + bill.address = "TestAddress" + bill.storeName = "StoreName" + bill.salesTax = 19.0 + + // insert the bill + bill.id = headDao.insert(bill) + + // insert numItems items + var lastInsertedItemId = 0L + for (i in 1..numItems) { + + // new item with bill id of previously inserted bill + val item = Item() + item.billId = bill.id + item.amount = i.toDouble() + item.name = "TestItem#$i" + item.nettoPrice = i.toDouble() + + // insert item + item.id = itemDao.insert(item) + lastInsertedItemId = item.id + } + + // get bill with items + var actual = headDao.getBillById(bill.id) + + // bill should have numItems items + assertThat(actual.items.size, equalTo(numItems)) + + // delete last inserted item + val item = itemDao.getById(lastInsertedItemId) + itemDao.delete(item) + + // get bill with items, should now have numItems-1 items + actual = headDao.getBillById(bill.id) + assertThat(actual.items.size, equalTo(numItems-1)) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/easybill/database/ItemTest.kt b/app/src/androidTest/java/com/easybill/database/ItemTest.kt new file mode 100644 index 0000000..1a587bb --- /dev/null +++ b/app/src/androidTest/java/com/easybill/database/ItemTest.kt @@ -0,0 +1,107 @@ +package com.easybill.database + +import android.content.Context +import androidx.room.Room +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.easybill.database.dao.ItemDao +import com.easybill.database.model.Item +import org.hamcrest.CoreMatchers.* +import org.junit.After +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import java.io.IOException + +@RunWith(AndroidJUnit4::class) +class ItemTest { + private lateinit var itemDao: ItemDao + private lateinit var db: EasyBillDatabase + + @Before + fun createDb() { + val context = ApplicationProvider.getApplicationContext() + db = Room.inMemoryDatabaseBuilder( + context, EasyBillDatabase::class.java).build() + itemDao = db.getItemDao() + } + + @After + @Throws(IOException::class) + fun closeDb() { + db.close() + } + + @Test + @Throws(Exception::class) + fun insertAndGetTenItems() { + val numItems = 10 + + for (i in 1..numItems) { + // create a new item with test-data + val item = Item() + item.name = "ItemName#$i" + item.amount = i.toDouble() + item.nettoPrice = i.toDouble() + + // insert the item and get it + item.id = itemDao.insert(item) + val actual = itemDao.getById(item.id) + + // verify results + assertThat(actual.name, equalTo(item.name)) + assertThat(actual.amount, equalTo(item.amount)) + assertThat(actual.nettoPrice, equalTo(item.nettoPrice)) + } + + // get all items + val items = itemDao.getAll() + assertThat(items.size, equalTo(numItems)) + } + + @Test + @Throws(Exception::class) + fun insertAndUpdateItem() { + // create a new item with test-data + val item = Item() + item.name = "TestName" + item.amount = 1.0 + item.nettoPrice = 1.0 + + // insert the item + item.id = itemDao.insert(item) + + // now change the item and update it + item.name = "NewTestName" + item.amount = 2.0 + item.nettoPrice = 10.0 + itemDao.update(item) + + // get the updated item and verify + val actual = itemDao.getById(item.id) + assertThat(actual.name, equalTo(item.name)) + assertThat(actual.amount, equalTo(item.amount)) + assertThat(actual.nettoPrice, equalTo(item.nettoPrice)) + } + + @Test + @Throws(Exception::class) + fun insertAndDeleteItem() { + // create a new item with test-data + val item = Item() + item.name = "TestName" + item.amount = 1.0 + item.nettoPrice = 1.0 + + // insert the item + item.id = itemDao.insert(item) + + // delete the item + itemDao.delete(item) + + // the item should now be deleted and null must be returned + val actual = itemDao.getById(item.id) + assertThat(actual, `is`(nullValue())) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 66c8fd3..249fd8f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,9 @@ + package="com.easybill"> - diff --git a/app/src/main/ic_launcher-playstore.png b/app/src/main/ic_launcher-playstore.png new file mode 100644 index 0000000..07431c7 Binary files /dev/null and b/app/src/main/ic_launcher-playstore.png differ diff --git a/app/src/main/java/com/easybill/EasyBillApplication.kt b/app/src/main/java/com/easybill/EasyBillApplication.kt new file mode 100644 index 0000000..8ffc207 --- /dev/null +++ b/app/src/main/java/com/easybill/EasyBillApplication.kt @@ -0,0 +1,16 @@ +package com.easybill + +import android.app.Application +import timber.log.Timber + +/** + * Entry-point of the application. + */ +class EasyBillApplication : Application() { + override fun onCreate() { + super.onCreate() + + // setup timber for logging + Timber.plant(Timber.DebugTree()) + } +} diff --git a/app/src/main/java/com/easybill/MainActivity.kt b/app/src/main/java/com/easybill/MainActivity.kt new file mode 100644 index 0000000..3eca750 --- /dev/null +++ b/app/src/main/java/com/easybill/MainActivity.kt @@ -0,0 +1,156 @@ +package com.easybill + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import androidx.navigation.ui.NavigationUI +import com.easybill.database.Converters +import com.easybill.database.EasyBillDatabase +import com.easybill.database.model.Bill +import com.easybill.database.model.Head +import com.easybill.database.model.Item +import com.easybill.viewmodel.EasyBillViewModel +import com.easybill.viewmodel.EasyBillViewModelFactory +import java.util.Objects.requireNonNull + +/** + * Main-Activity of this application. + */ +class MainActivity : AppCompatActivity() { + + // keeps state when the activity gets re-loaded on device configuration change + private lateinit var viewModel: EasyBillViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + // setup navigation + val navController = this.findNavController(R.id.myNavHostFragment) + NavigationUI.setupActionBarWithNavController(this, navController) + + // get bill-dao and create view-model + val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + val billDao = EasyBillDatabase.getInstance(application).getBillDao() + + // create view-model + val viewModelFactory = EasyBillViewModelFactory(headDao, itemDao, billDao, application) + viewModel = ViewModelProvider(this, viewModelFactory).get(EasyBillViewModel::class.java) + + // fill database with mock-data + // fillDatabase() + + // clear database-tables + // clearDatabase() + } + + override fun onSupportNavigateUp(): Boolean { + val navController = this.findNavController(R.id.myNavHostFragment) + return navController.navigateUp() + } + + private fun fillDatabase() { + val millisPerDay = 24 * 60 * 60 * 1000 + val converter = Converters() + // val application = requireNonNull(this).application + // val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + // val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + // val billDao = EasyBillDatabase.getInstance(application).getBillDao() + // val viewModel = EasyBillViewModel(headDao, itemDao, billDao, application) + + /* + * Bill#1 + */ + val headOne = Head() + headOne.storeName = "Media Markt" + headOne.address = "Fakestreet 1234, 80801 München" + headOne.time = + converter.localDateTimeFromTimestamp(System.currentTimeMillis() - millisPerDay)!! + // items #1.1 + val itemOneOne = Item() + itemOneOne.amount = 1.0 + itemOneOne.tax = 0.19 + itemOneOne.name = "USB-Stick erster Güte" + itemOneOne.nettoPrice = 13.37 + // items #1.2 + val itemOneTwo = Item() + itemOneTwo.amount = 3.0 + itemOneTwo.tax = 0.19 + itemOneTwo.name = "Wirklich schnelle SSD" + itemOneTwo.nettoPrice = 199.00 + val billOne = Bill( + headOne, + listOf(itemOneOne, itemOneTwo) + ) + viewModel.addBill(billOne) + + /* + * Bill#2 + */ + val headTwo = Head() + headTwo.storeName = "Edeka" + headTwo.address = "Irgendwostr. 13, 13370 Frankfurt" + headTwo.time = converter + .localDateTimeFromTimestamp(System.currentTimeMillis() - 2 * millisPerDay)!! + // items #2.1 + val itemTwoOne = Item() + itemTwoOne.amount = 1.0 + itemTwoOne.tax = 0.07 + itemTwoOne.name = "Käsebrot" + itemTwoOne.nettoPrice = 7.99 + // items #2.2 + val itemTwoTwo = Item() + itemTwoTwo.amount = 3.0 + itemTwoTwo.tax = 0.07 + itemTwoTwo.name = "Milch von der Kuh" + itemTwoTwo.nettoPrice = 1.39 + // items #2.3 + val itemTwoThree = Item() + itemTwoThree.amount = 3.0 + itemTwoThree.tax = 0.07 + itemTwoThree.name = "Magerquark" + itemTwoThree.nettoPrice = 0.89 + val billTwo = Bill( + headTwo, + listOf(itemTwoOne, itemTwoTwo, itemTwoThree) + ) + viewModel.addBill(billTwo) + + /* + * Bill#3 + */ + val headThree = Head() + headThree.storeName = "McDonalds" + headThree.address = "Hipstersquare 13, 08574 Bonn" + headThree.time = converter + .localDateTimeFromTimestamp(System.currentTimeMillis() - 3 * millisPerDay)!! + // items #3.1 + val itemThreeOne = Item() + itemThreeOne.amount = 199.0 + itemThreeOne.tax = 0.07 + itemThreeOne.name = "Cheeseburger" + itemThreeOne.nettoPrice = 1.0 + // items #3.2 + val itemThreeTwo = Item() + itemThreeTwo.amount = 1.0 + itemThreeTwo.tax = 0.07 + itemThreeTwo.name = "Diät-Cola" + itemThreeTwo.nettoPrice = 1.0 + val billThree = Bill( + headThree, + listOf(itemThreeOne, itemThreeTwo) + ) + viewModel.addBill(billThree) + } + + private fun clearDatabase() { + val application = requireNonNull(this).application + val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + val billDao = EasyBillDatabase.getInstance(application).getBillDao() + val viewModel = EasyBillViewModel(headDao, itemDao, billDao, application) + viewModel.deleteAllBills() + } +} diff --git a/app/src/main/java/com/easybill/database/Converters.kt b/app/src/main/java/com/easybill/database/Converters.kt new file mode 100644 index 0000000..70cdc6a --- /dev/null +++ b/app/src/main/java/com/easybill/database/Converters.kt @@ -0,0 +1,37 @@ +package com.easybill.database + +import androidx.room.TypeConverter +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.TimeZone + +/** + * Various converters to translate non-primitive types to a fitting + * database-representation. + */ +class Converters { + + /** + * Converts a timestamp (milliseconds) to a LocalDateTime of the default time-zone. + */ + @TypeConverter + fun localDateTimeFromTimestamp(value: Long?): LocalDateTime? { + return value?.let { + LocalDateTime.ofInstant( + Instant.ofEpochMilli(value), + TimeZone.getDefault().toZoneId() + ) + } + } + + /** + * Converts a LocalDateTime to a timestamp (milliseconds) of the default time-zone. + */ + @TypeConverter + fun localDateTimeToTimestamp(value: LocalDateTime?): Long? { + return value?.let { + value.atZone(ZoneId.systemDefault()).toEpochSecond() + } + } +} diff --git a/app/src/main/java/com/easybill/database/EasyBillDatabase.kt b/app/src/main/java/com/easybill/database/EasyBillDatabase.kt new file mode 100644 index 0000000..a4948a3 --- /dev/null +++ b/app/src/main/java/com/easybill/database/EasyBillDatabase.kt @@ -0,0 +1,70 @@ +package com.easybill.database + +import android.content.Context +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase +import androidx.room.TypeConverters +import com.easybill.database.dao.BillDao +import com.easybill.database.dao.HeadDao +import com.easybill.database.dao.ItemDao +import com.easybill.database.model.Head +import com.easybill.database.model.Item + +/** + * Provides access to the database through a singleton-object. + */ +@Database( + entities = [Head::class, Item::class], + version = 1, + exportSchema = false +) +@TypeConverters(Converters::class) +abstract class EasyBillDatabase : RoomDatabase() { + + /** + * Getter for the HeadDao. + */ + abstract fun getHeadDao(): HeadDao + + /** + * Getter for the ItemDao. + */ + abstract fun getItemDao(): ItemDao + + /** + * Getter for the BillDao. + */ + abstract fun getBillDao(): BillDao + + /** + * Keeps the singleton. + */ + companion object { + + @Volatile + private var instance: EasyBillDatabase? = null + + /** + * Provides thread-safe access to the EasyBillDatabase singleton-object. + */ + fun getInstance(context: Context): EasyBillDatabase { + val tmp = instance + if (tmp != null) + return tmp + + return synchronized(this) { + if (instance != null) { + instance!! + } else { + instance = Room.databaseBuilder( + context.applicationContext, + EasyBillDatabase::class.java, + "easyBillDatabase" + ).build() + instance!! + } + } + } + } +} diff --git a/app/src/main/java/com/easybill/database/dao/BillDao.kt b/app/src/main/java/com/easybill/database/dao/BillDao.kt new file mode 100644 index 0000000..7c432df --- /dev/null +++ b/app/src/main/java/com/easybill/database/dao/BillDao.kt @@ -0,0 +1,20 @@ +package com.easybill.database.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.Transaction +import com.easybill.database.model.Bill + +@Dao +interface BillDao { + @Transaction + @Query("SELECT * FROM head WHERE id = :key") + fun getBillById(key: Long): Bill + + @Transaction + @Query("SELECT * FROM head") + fun getAllBills(): MutableList + + @Query("DELETE FROM head") + fun deleteAllBills() +} diff --git a/app/src/main/java/com/easybill/database/dao/HeadDao.kt b/app/src/main/java/com/easybill/database/dao/HeadDao.kt new file mode 100644 index 0000000..b725319 --- /dev/null +++ b/app/src/main/java/com/easybill/database/dao/HeadDao.kt @@ -0,0 +1,33 @@ +package com.easybill.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.easybill.database.model.Head + +/** + * Provides CRUD-Operations to the Head-Entity. + */ +@Dao +interface HeadDao { + + @Insert + fun insert(head: Head): Long + + @Update + fun update(head: Head) + + @Delete + fun delete(head: Head) + + @Query("DELETE FROM head WHERE id = :id") + fun deleteById(id: Long) + + @Query("SELECT * FROM head WHERE id = :id") + fun getById(id: Long): Head + + @Query("SELECT * FROM head") + fun getAll(): MutableList +} diff --git a/app/src/main/java/com/easybill/database/dao/ItemDao.kt b/app/src/main/java/com/easybill/database/dao/ItemDao.kt new file mode 100644 index 0000000..8cf7ab8 --- /dev/null +++ b/app/src/main/java/com/easybill/database/dao/ItemDao.kt @@ -0,0 +1,30 @@ +package com.easybill.database.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.Query +import androidx.room.Update +import com.easybill.database.model.Item + +/** + * Provides CRUD-Operations to the BillItem-Entity. + */ +@Dao +interface ItemDao { + + @Insert + fun insert(item: Item): Long + + @Update + fun update(item: Item) + + @Delete + fun delete(item: Item) + + @Query("SELECT * from item WHERE id = :id") + fun getById(id: Long): Item + + @Query("SELECT * FROM item") + fun getAll(): List +} diff --git a/app/src/main/java/com/easybill/database/model/Bill.kt b/app/src/main/java/com/easybill/database/model/Bill.kt new file mode 100644 index 0000000..48d6c21 --- /dev/null +++ b/app/src/main/java/com/easybill/database/model/Bill.kt @@ -0,0 +1,32 @@ +package com.easybill.database.model + +import androidx.room.Embedded +import androidx.room.Relation + +/** + * A Bill consists of a head and a list of items. + */ +data class Bill( + @Embedded val head: Head, + + @Relation(parentColumn = "id", entityColumn = "billId") + val items: List +) { + override fun toString(): String { + var total = 0.0 + for (item in items) { + total += item.bruttoPrice() + } + var head = String.format("Store: %s\nAddress: %s\nTime: %s\nTotal: %s\n\nItems:\n", + head.storeName, head.address, head.time, total) + + for (item in items) { + head += + String.format("\n%s\n\tamount: %f\n\ttax: %f\n\tnetto: %f\n\tbrutto=%f\n\ttotal=%f", + item.name, item.amount, item.tax, + item.nettoPrice, item.bruttoPrice(), item.totalPrice()) + } + + return head + } +} diff --git a/app/src/main/java/com/easybill/database/model/Head.kt b/app/src/main/java/com/easybill/database/model/Head.kt new file mode 100644 index 0000000..76d859b --- /dev/null +++ b/app/src/main/java/com/easybill/database/model/Head.kt @@ -0,0 +1,31 @@ +package com.easybill.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.time.LocalDateTime + +/** + * A head contains the top-section of a bill, e.g. the bills meta-data. + */ +@Entity(tableName = "head") +data class Head( + + @PrimaryKey(autoGenerate = true) + var id: Long = 0L, + + /** + * The address of the store where the bill was obtained. + */ + var address: String = "", + + /** + * The name of the store where the bill was obtained. + */ + var storeName: String = "", + + /** + * The time that the bill was printed. + */ + var time: LocalDateTime = LocalDateTime.now() + +) diff --git a/app/src/main/java/com/easybill/database/model/Item.kt b/app/src/main/java/com/easybill/database/model/Item.kt new file mode 100644 index 0000000..8d12d07 --- /dev/null +++ b/app/src/main/java/com/easybill/database/model/Item.kt @@ -0,0 +1,50 @@ +package com.easybill.database.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * An Item contains the data of a single item/position included on a bill. + */ +@Entity(tableName = "item") +class Item( + + @PrimaryKey(autoGenerate = true) + var id: Long = 0L, + + /** + * The Bill this item is associated with. + */ + var billId: Long = 0L, + + /** + * The name of the item. + */ + var name: String = "", + + /** + * The amount of the item (pcs, weight). + */ + var amount: Double = 0.0, + + /** + * The tax rate on the item. + */ + var tax: Double = 0.0, + + /** + * The price of a single item of this kind (without tax) + */ + var nettoPrice: Double = 0.0 +) { + + /** + * Calculates the brutto-price of the item, e.g. nettoPrice * (1 + tax) + */ + fun bruttoPrice(): Double = nettoPrice * (1 + tax) + + /** + * Calculates the total price of the item, e.g. amount * unit. + */ + fun totalPrice(): Double = bruttoPrice() * amount +} diff --git a/app/src/main/java/com/easybill/fragments/DetailedBillFragment.kt b/app/src/main/java/com/easybill/fragments/DetailedBillFragment.kt new file mode 100644 index 0000000..42f520d --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/DetailedBillFragment.kt @@ -0,0 +1,80 @@ +package com.easybill.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import com.easybill.R +import com.easybill.database.EasyBillDatabase +import com.easybill.database.model.Bill +import com.easybill.databinding.DetailedBillBinding +import com.easybill.viewmodel.EasyBillViewModel +import com.easybill.viewmodel.EasyBillViewModelFactory + +/** + * Shows a bill with all its information. + */ +class DetailedBillFragment : Fragment() { + + // keeps state when the activity gets re-loaded on device configuration change + private lateinit var viewModel: EasyBillViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // get bill-dao and create view-model + val application = activity?.application + if (application != null) { + val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + val billDao = EasyBillDatabase.getInstance(application).getBillDao() + + // create view-model + val viewModelFactory = EasyBillViewModelFactory(headDao, itemDao, billDao, application) + viewModel = ViewModelProvider(activity!!, viewModelFactory) + .get(EasyBillViewModel::class.java) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + // binding + val binding: DetailedBillBinding = + DataBindingUtil.inflate(inflater, R.layout.detailed_bill, container, false) + + // get bill-id + val billId = DetailedBillFragmentArgs.fromBundle(requireArguments()).billID + + // find correct bill + var bill: Bill? = null + if (viewModel.bills.value != null) + bill = viewModel.bills.value!!.find { it.head.id == billId } + + if (bill != null) { + + /* + * Delete bill + */ + binding.datailedBillDeleteButton.setOnClickListener { + viewModel.deleteBillById(billId) + it.findNavController().navigate(R.id.action_detailedBillFragment_to_archiveFragment) + } + + /* + * Set details + */ + binding.detailedBillTitle.text = bill.head.storeName + binding.detailedBillOutput.text = bill.toString() + } + + return binding.root + } +} diff --git a/app/src/main/java/com/easybill/fragments/FilterFragment.kt b/app/src/main/java/com/easybill/fragments/FilterFragment.kt new file mode 100644 index 0000000..d6cba08 --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/FilterFragment.kt @@ -0,0 +1,34 @@ +package com.easybill.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.navigation.findNavController +import com.easybill.R +import com.easybill.databinding.FilterBinding + +/** + * Lets the user set filters to find a subset of bills. + */ +class FilterFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + val binding: FilterBinding = DataBindingUtil.inflate( + inflater, R.layout.filter, container, false) + + binding.applyButton.setOnClickListener { + // TODO check if everything is right + it.findNavController().navigate(R.id.action_filterFragment_to_archiveFragment) + } + + return binding.root + } +} diff --git a/app/src/main/java/com/easybill/fragments/ScanFragment.kt b/app/src/main/java/com/easybill/fragments/ScanFragment.kt new file mode 100644 index 0000000..2517bb9 --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/ScanFragment.kt @@ -0,0 +1,93 @@ +package com.easybill.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import com.easybill.R +import com.easybill.database.Converters +import com.easybill.database.EasyBillDatabase +import com.easybill.database.model.Bill +import com.easybill.database.model.Head +import com.easybill.database.model.Item +import com.easybill.databinding.ScanBinding +import com.easybill.viewmodel.EasyBillViewModel +import com.easybill.viewmodel.EasyBillViewModelFactory + +/** + * Let's the user scan/make a photo of a new bill. + */ +class ScanFragment : Fragment() { + + // keeps state when the activity gets re-loaded on device configuration change + private lateinit var viewModel: EasyBillViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // get bill-dao and create view-model + val application = activity?.application + if (application != null) { + val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + val billDao = EasyBillDatabase.getInstance(application).getBillDao() + + // create view-model + val viewModelFactory = EasyBillViewModelFactory(headDao, itemDao, billDao, application) + viewModel = ViewModelProvider(activity!!, viewModelFactory) + .get(EasyBillViewModel::class.java) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + val binding: ScanBinding = DataBindingUtil.inflate( + inflater, R.layout.scan, container, false + ) + + binding.scanButton.setOnClickListener { + // TODO scan bill and add to database + + // create new mock-bill + val millisPerDay = 24 * 60 * 60 * 1000 + val converter = Converters() + /* + * Bill#1 + */ + val headOne = Head() + headOne.storeName = "Media Markt" + headOne.address = "Fakestreet 1234, 80801 München" + headOne.time = + converter.localDateTimeFromTimestamp(System.currentTimeMillis() - millisPerDay)!! + // items #1.1 + val itemOneOne = Item() + itemOneOne.amount = 1.0 + itemOneOne.tax = 0.19 + itemOneOne.name = "USB-Stick erster Güte" + itemOneOne.nettoPrice = 13.37 + // items #1.2 + val itemOneTwo = Item() + itemOneTwo.amount = 3.0 + itemOneTwo.tax = 0.19 + itemOneTwo.name = "Wirklich schnelle SSD" + itemOneTwo.nettoPrice = 199.00 + val billOne = Bill( + headOne, + listOf(itemOneOne, itemOneTwo) + ) + viewModel.addBill(billOne) + + it.findNavController().navigate(R.id.action_scanFragment_to_archiveFragment) + } + + return binding.root + } +} diff --git a/app/src/main/java/com/easybill/fragments/StatisticsFragment.kt b/app/src/main/java/com/easybill/fragments/StatisticsFragment.kt new file mode 100644 index 0000000..1171fae --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/StatisticsFragment.kt @@ -0,0 +1,28 @@ +package com.easybill.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import com.easybill.R +import com.easybill.databinding.StatisticBinding + +/** + * Shows statistics of all bills. + */ +class StatisticsFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + val binding: StatisticBinding = DataBindingUtil.inflate( + inflater, R.layout.statistic, container, false) + + return binding.root + } +} diff --git a/app/src/main/java/com/easybill/fragments/archive/ArchiveAdapter.kt b/app/src/main/java/com/easybill/fragments/archive/ArchiveAdapter.kt new file mode 100644 index 0000000..0c21546 --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/archive/ArchiveAdapter.kt @@ -0,0 +1,53 @@ +package com.easybill.fragments.archive + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.navigation.findNavController +import androidx.recyclerview.widget.RecyclerView +import com.easybill.R +import com.easybill.database.model.Bill +import com.easybill.viewmodel.EasyBillViewModel +import kotlinx.android.synthetic.main.archive_listview_item.view.* + +class ArchiveAdapter(private var viewModel: EasyBillViewModel) : + RecyclerView.Adapter() { + + inner class BillsWithItemsViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + fun bind(bill: Bill) { + itemView.list_view_company_name.text = bill.head.storeName + itemView.list_view_date.text = bill.head.time.toString() + + var total = 0.0 + for (item in bill.items) { + total += item.nettoPrice * item.amount + } + itemView.list_view_price.text = "$total €" + + itemView.setOnClickListener { + it.findNavController().navigate(ArchiveFragmentDirections + .actionArchiveFragmentToDetailedBillFragment(bill.head.id)) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BillsWithItemsViewHolder { + val itemView = + LayoutInflater.from(parent.context) + .inflate(R.layout.archive_listview_item, parent, false) + + return BillsWithItemsViewHolder(itemView) + } + + override fun getItemCount(): Int { + // Timber.i("item count is %d (%s)", viewModel.bills.value?.size?: 0, viewModel.bills.value) + return viewModel.bills.value?.size ?: 0 + } + + override fun onBindViewHolder(holder: BillsWithItemsViewHolder, position: Int) { + val tmp = viewModel.bills.value?.get(position) + // Timber.i("tmp is %s", tmp) + if (tmp != null) + holder.bind(tmp) + } +} diff --git a/app/src/main/java/com/easybill/fragments/archive/ArchiveFragment.kt b/app/src/main/java/com/easybill/fragments/archive/ArchiveFragment.kt new file mode 100644 index 0000000..9f61040 --- /dev/null +++ b/app/src/main/java/com/easybill/fragments/archive/ArchiveFragment.kt @@ -0,0 +1,136 @@ +package com.easybill.fragments.archive + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModelProvider +import androidx.navigation.findNavController +import androidx.recyclerview.widget.LinearLayoutManager +import com.easybill.R +import com.easybill.database.EasyBillDatabase +import com.easybill.databinding.ArchiveBinding +import com.easybill.viewmodel.EasyBillViewModel +import com.easybill.viewmodel.EasyBillViewModelFactory + +/** + * Displays the bill-archive. This is the first fragment that is shown to the user + * when the application is started. + */ +class ArchiveFragment : Fragment() { + + // data-binding for this activity + private lateinit var binding: ArchiveBinding + + // keeps state when the activity gets re-loaded on device configuration change + private lateinit var viewModel: EasyBillViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.inflate( + layoutInflater, R.layout.archive, null, false) + + // get bill-dao and create view-model + val application = activity?.application + if (application != null) { + val headDao = EasyBillDatabase.getInstance(application).getHeadDao() + val itemDao = EasyBillDatabase.getInstance(application).getItemDao() + val billDao = EasyBillDatabase.getInstance(application).getBillDao() + + // create view-model + val viewModelFactory = EasyBillViewModelFactory(headDao, itemDao, billDao, application) + viewModel = ViewModelProvider(activity!!, viewModelFactory) + .get(EasyBillViewModel::class.java) + } + + // set bindings view-model + binding.billViewModel = viewModel + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + + // setup recycler-view + // activity?.findViewById(R.id.archiveRecyclerView) + binding.archiveRecyclerView.layoutManager = LinearLayoutManager(context) + viewModel.bills.observe(viewLifecycleOwner, Observer { + binding.archiveRecyclerView.adapter = + ArchiveAdapter(viewModel) + }) + + viewModel.getAllBills() + + /* + * Navigate to ScanFragment + */ + binding.buttonAdd.setOnClickListener { view: View -> + view.findNavController().navigate(R.id.action_archiveFragment_to_scanFragment) + } + + /* + * Navigate to FilterFragment + */ + binding.buttonFilter.setOnClickListener { + closeButtonMenu() + it.findNavController().navigate(R.id.action_archiveFragment_to_filterFragment) + } + + /* + * Open/close menu + */ + binding.buttonMenu.setOnClickListener { + onMenuButtonClicked() + } + + /* + * Sort by date + */ + binding.buttonSortByDate.setOnClickListener { + // TODO: get bills from database sorted by date + closeButtonMenu() + Toast.makeText(this.activity, "Sorted by date", Toast.LENGTH_LONG).show() + } + + /* + * Sort by price + */ + binding.buttonSortByPrice.setOnClickListener { + // TODO: get bills from database sorted by price + closeButtonMenu() + Toast.makeText(this.activity, "Sorted by price", Toast.LENGTH_LONG).show() + } + + /* + * Navigate to statistics + */ + binding.buttonStatistics.setOnClickListener { + closeButtonMenu() + it.findNavController().navigate(R.id.action_archiveFragment_to_statisticsFragment) + } + + return binding.root + } + + private fun onMenuButtonClicked() { + if (binding.buttonsList.visibility == View.INVISIBLE) + openButtonMenu() + else + closeButtonMenu() + } + + private fun openButtonMenu() { + binding.buttonsList.visibility = View.VISIBLE + } + + private fun closeButtonMenu() { + binding.buttonsList.visibility = View.INVISIBLE + } +} diff --git a/app/src/main/java/com/easybill/viewmodel/EasyBillViewModel.kt b/app/src/main/java/com/easybill/viewmodel/EasyBillViewModel.kt new file mode 100644 index 0000000..bee2459 --- /dev/null +++ b/app/src/main/java/com/easybill/viewmodel/EasyBillViewModel.kt @@ -0,0 +1,98 @@ +package com.easybill.viewmodel + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import com.easybill.database.dao.BillDao +import com.easybill.database.dao.HeadDao +import com.easybill.database.dao.ItemDao +import com.easybill.database.model.Bill +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class EasyBillViewModel( + private val headDao: HeadDao, + private val itemDao: ItemDao, + private val billDao: BillDao, + application: Application +) : AndroidViewModel(application) { + + // scope & job for coroutines + private var job: Job = Job() + private var uiScope: CoroutineScope = CoroutineScope(Dispatchers.Main + job) + + // keeps/caches all bills + private var privBills = MutableLiveData>() + val bills: LiveData> get() = privBills + + init { + privBills.value = mutableListOf() + getAllBills() // get all bills from database to populate cache + } + + /** + * Add a bill. + */ + fun addBill(bill: Bill) = uiScope.launch { + addBillWithItemsToDatabase(bill) + } + + private suspend fun addBillWithItemsToDatabase(bill: Bill) = + withContext(Dispatchers.IO) { + bill.head.id = headDao.insert(bill.head) + + for (item in bill.items) { + item.billId = bill.head.id + item.id = itemDao.insert(item) + } + + if (!privBills.value!!.any { b -> b.head.id == bill.head.id }) { + privBills.value?.add(bill) + } + } + + /** + * Get all bills. + */ + fun getAllBills() = uiScope.launch { + suspendGetAllBills() + } + + private suspend fun suspendGetAllBills() = withContext(Dispatchers.IO) { + val allBills = billDao.getAllBills() + + for (bill in allBills) { + if (!privBills.value!!.any { b -> b.head.id == bill.head.id }) { + privBills.value!!.add(bill) + } + } + } + + /** + * Delete a Bill by Id. + */ + fun deleteBillById(id: Long) = uiScope.launch { + suspendDeleteBillById(id) + } + + private suspend fun suspendDeleteBillById(id: Long) = withContext(Dispatchers.IO) { + headDao.deleteById(id) + privBills.value?.removeIf { it.head.id == id } + } + + /** + * Delete all Bills (head & items). + */ + fun deleteAllBills() = uiScope.launch { + suspendedDeleteAllBills() + } + + private suspend fun suspendedDeleteAllBills() = withContext(Dispatchers.IO) { + billDao.deleteAllBills() + privBills.value?.clear() + } +} diff --git a/app/src/main/java/com/easybill/viewmodel/EasyBillViewModelFactory.kt b/app/src/main/java/com/easybill/viewmodel/EasyBillViewModelFactory.kt new file mode 100644 index 0000000..22c19f7 --- /dev/null +++ b/app/src/main/java/com/easybill/viewmodel/EasyBillViewModelFactory.kt @@ -0,0 +1,25 @@ +package com.easybill.viewmodel + +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.easybill.database.dao.BillDao +import com.easybill.database.dao.HeadDao +import com.easybill.database.dao.ItemDao +import java.lang.IllegalArgumentException + +@Suppress("UNCHECKED_CAST") +class EasyBillViewModelFactory( + private val headDao: HeadDao, + private val itemDao: ItemDao, + private val billDao: BillDao, + private val application: Application +) : ViewModelProvider.Factory { + + override fun create(modelClass: Class): T { + if (modelClass.isAssignableFrom(EasyBillViewModel::class.java)) + return EasyBillViewModel(headDao, itemDao, billDao, application) as T + + throw IllegalArgumentException("Unknown ViewModel class") + } +} diff --git a/app/src/main/java/com/example/easybill/MainActivity.kt b/app/src/main/java/com/example/easybill/MainActivity.kt deleted file mode 100644 index 58a1ddf..0000000 --- a/app/src/main/java/com/example/easybill/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.easybill - -import androidx.appcompat.app.AppCompatActivity -import android.os.Bundle - -class MainActivity : AppCompatActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - } -} diff --git a/app/src/main/res/drawable-anydpi/ic_add.xml b/app/src/main/res/drawable-anydpi/ic_add.xml new file mode 100644 index 0000000..fa79705 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_add.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_calendar.xml b/app/src/main/res/drawable-anydpi/ic_calendar.xml new file mode 100644 index 0000000..85ae787 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_calendar.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_filter.xml b/app/src/main/res/drawable-anydpi/ic_filter.xml new file mode 100644 index 0000000..2265cbd --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_filter.xml @@ -0,0 +1,14 @@ + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_menu.xml b/app/src/main/res/drawable-anydpi/ic_menu.xml new file mode 100644 index 0000000..28b7856 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_menu.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable-anydpi/ic_money.xml b/app/src/main/res/drawable-anydpi/ic_money.xml new file mode 100644 index 0000000..b23475a --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_money.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-anydpi/ic_statistic.xml b/app/src/main/res/drawable-anydpi/ic_statistic.xml new file mode 100644 index 0000000..0a50d70 --- /dev/null +++ b/app/src/main/res/drawable-anydpi/ic_statistic.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable-hdpi/ic_add.png b/app/src/main/res/drawable-hdpi/ic_add.png new file mode 100644 index 0000000..2b125f5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_calendar.png b/app/src/main/res/drawable-hdpi/ic_calendar.png new file mode 100644 index 0000000..7f946ea Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_calendar.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu.png b/app/src/main/res/drawable-hdpi/ic_menu.png new file mode 100644 index 0000000..beeda13 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_menu.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_money.png b/app/src/main/res/drawable-hdpi/ic_money.png new file mode 100644 index 0000000..7dcb26d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_money.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_statistic.png b/app/src/main/res/drawable-hdpi/ic_statistic.png new file mode 100644 index 0000000..0c7f4bc Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_statistic.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_add.png b/app/src/main/res/drawable-mdpi/ic_add.png new file mode 100644 index 0000000..a69d739 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_calendar.png b/app/src/main/res/drawable-mdpi/ic_calendar.png new file mode 100644 index 0000000..c388a9a Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_calendar.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_menu.png b/app/src/main/res/drawable-mdpi/ic_menu.png new file mode 100644 index 0000000..36978ca Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_menu.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_money.png b/app/src/main/res/drawable-mdpi/ic_money.png new file mode 100644 index 0000000..ba214f7 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_money.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_statistic.png b/app/src/main/res/drawable-mdpi/ic_statistic.png new file mode 100644 index 0000000..fe82985 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_statistic.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_add.png b/app/src/main/res/drawable-xhdpi/ic_add.png new file mode 100644 index 0000000..4423088 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_calendar.png b/app/src/main/res/drawable-xhdpi/ic_calendar.png new file mode 100644 index 0000000..3db9cd6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_calendar.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_filter.png b/app/src/main/res/drawable-xhdpi/ic_filter.png new file mode 100644 index 0000000..daa1ba6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_filter.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_menu.png b/app/src/main/res/drawable-xhdpi/ic_menu.png new file mode 100644 index 0000000..f25b18e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_menu.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_money.png b/app/src/main/res/drawable-xhdpi/ic_money.png new file mode 100644 index 0000000..69e87c0 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_money.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_statistic.png b/app/src/main/res/drawable-xhdpi/ic_statistic.png new file mode 100644 index 0000000..0aeebce Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_statistic.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_add.png b/app/src/main/res/drawable-xxhdpi/ic_add.png new file mode 100644 index 0000000..94b04c8 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_add.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_calendar.png b/app/src/main/res/drawable-xxhdpi/ic_calendar.png new file mode 100644 index 0000000..d3b6b40 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_calendar.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_menu.png b/app/src/main/res/drawable-xxhdpi/ic_menu.png new file mode 100644 index 0000000..4716e10 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_menu.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_money.png b/app/src/main/res/drawable-xxhdpi/ic_money.png new file mode 100644 index 0000000..c8101dc Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_money.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_statistic.png b/app/src/main/res/drawable-xxhdpi/ic_statistic.png new file mode 100644 index 0000000..4520ca4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_statistic.png differ diff --git a/app/src/main/res/drawable/buttons_background.xml b/app/src/main/res/drawable/buttons_background.xml new file mode 100644 index 0000000..f900259 --- /dev/null +++ b/app/src/main/res/drawable/buttons_background.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_shadow_button.xml b/app/src/main/res/drawable/shape_shadow_button.xml new file mode 100644 index 0000000..7dc80bb --- /dev/null +++ b/app/src/main/res/drawable/shape_shadow_button.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4fc2444..23c0eb2 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -6,13 +6,12 @@ android:layout_height="match_parent" tools:context=".MainActivity"> - + \ No newline at end of file diff --git a/app/src/main/res/layout/archive.xml b/app/src/main/res/layout/archive.xml new file mode 100644 index 0000000..9d18cae --- /dev/null +++ b/app/src/main/res/layout/archive.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/archive_listview_item.xml b/app/src/main/res/layout/archive_listview_item.xml new file mode 100644 index 0000000..45502cc --- /dev/null +++ b/app/src/main/res/layout/archive_listview_item.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/detailed_bill.xml b/app/src/main/res/layout/detailed_bill.xml new file mode 100644 index 0000000..0d51a4d --- /dev/null +++ b/app/src/main/res/layout/detailed_bill.xml @@ -0,0 +1,46 @@ + + + + + + + + +