diff --git a/.circleci/config.yml b/.circleci/config.yml index 4175da6..342196f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,26 +1,130 @@ # Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/configuration-reference +# See: https://circleci.com/docs/2.0/configuration-reference +# For a detailed guide to building and testing on Android, read the docs: +# https://circleci.com/docs/2.0/language-android/ for more details. version: 2.1 -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/configuration-reference/#jobs -jobs: - say-hello: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/configuration-reference/#executor-job - docker: - - image: cimg/base:stable - # Add steps to the job - # See: https://circleci.com/docs/configuration-reference/#steps +# Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects. +# See: https://circleci.com/docs/2.0/orb-intro/ +orbs: + android: circleci/android@2.3.0 + +commands: + setup_repo: + description: checkout repo and android dependencies steps: - checkout - run: - name: "Say hello" - command: "echo Hello, World!" - -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/configuration-reference/#workflows + name: Give gradle permissions + command: | + sudo chmod +x ./gradlew + - android/restore-gradle-cache + run_tests: + description: run tests for flavour specified + steps: + # The next step will run the unit tests + - run: + name: Run local unit tests + command: | + ./gradlew testDebugUnitTest + - android/save-gradle-cache + - store_artifacts: + path: app/build/reports + destination: reports + - store_test_results: + path: app/build/test-results + run_ui_tests: + description: run instrumentation and espresso tests + steps: + - android/start-emulator-and-run-tests: + post-emulator-launch-assemble-command: ./gradlew assembleAndroidTest + test-command: ./gradlew connectedDebugAndroidTest --continue + system-image: system-images;android-26;google_apis;x86 + # store screenshots for failed ui tests + - when: + condition: on_fail + steps: + - store_artifacts: + path: app/build/outputs/connected_android_test_additional_output/debugAndroidTest/connected + destination: connected_android_test + # store test reports + - store_artifacts: + path: app/build/reports/androidTests/connected + destination: reports + - store_test_results: + path: app/build/outputs/androidTest-results/connected + deploy_to_play_store: + description: deploy to playstore + steps: + # The next step will run the unit tests + - android/decode-keystore: + keystore-location: "./app/keystore.jks" + - run: + name: Setup playstore key + command: | + echo "$GOOGLE_PLAY_KEY" > "google-play-key.json" + - run: + name: Run fastlane command to deploy to playstore + command: | + pwd + bundle exec fastlane deploy + - store_test_results: + path: fastlane/report.xml +# Define a job to be invoked later in a workflow. +# See: https://circleci.com/docs/2.0/configuration-reference/#jobs +jobs: + # Below is the definition of your job to build and test your app, you can rename and customize it as you want. + build-and-test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_tests + run_instrumentation_test: + # These next lines define the Android machine image executor. + # See: https://circleci.com/docs/2.0/executor-types/ + executor: + name: android/android-machine + tag: 2023.05.1 + # Add steps to the job + # See: https://circleci.com/docs/2.0/configuration-reference/#steps + steps: + - setup_repo + - run_ui_tests + deploy-to-playstore: + docker: + - image: cimg/android:2023.07-browsers + auth: + username: ${DOCKER_USERNAME} + password: ${DOCKER_PASSWORD} + steps: + - setup_repo + - deploy_to_play_store +# Invoke jobs via workflows +# See: https://circleci.com/docs/2.0/configuration-reference/#workflows workflows: - say-hello-workflow: + version: 2 + build-release: jobs: - - say-hello + - build-and-test: + context: appttude + - run_instrumentation_test: + context: appttude + filters: + branches: + only: + - master + - release + - deploy-to-playstore: + context: appttude + filters: + branches: + only: + - release + requires: + - build-and-test \ No newline at end of file diff --git a/.gitignore b/.gitignore index 39fb081..ae7f176 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,97 @@ -*.iml +### AndroidStudio ### +# Covers files to be ignored for android development using Android Studio. + +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files .gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures +.gradle/ +build/ + +# Signing files +.signing/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio +/*/build/ +/*/local.properties +/*/out +/*/*/build +/*/*/production +captures/ +.navigation/ +*.ipr +*~ +*.swp + +# Keystore files +*.jks +*.keystore + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Android Patch +gen-external-apklibs + +# External native build folder generated in Android Studio 2.2 and later .externalNativeBuild + +# IntelliJ IDEA +*.iml +*.iws +/out/ + +# User-specific configurations +.idea/androidTestResultsUserPreferences.xml +.idea/caches/ +.idea/libraries/ +.idea/shelf/ +.idea/workspace.xml +.idea/tasks.xml +.idea/.name +.idea/compiler.xml +.idea/copyright/profiles_settings.xml +.idea/encodings.xml +.idea/misc.xml +.idea/modules.xml +.idea/scopes/scope_settings.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml +.idea/datasources.xml +.idea/dataSources.ids +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml +.idea/assetWizardSettings.xml +.idea/gradle.xml +.idea/jarRepositories.xml + +# Gem/fastlane +Gemfile.lock +/fastlane/report.xml +# Google play files +/google-play-key.json diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml deleted file mode 100644 index 263c04c..0000000 --- a/.idea/androidTestResultsUserPreferences.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/assetWizardSettings.xml b/.idea/assetWizardSettings.xml deleted file mode 100644 index 0068d43..0000000 --- a/.idea/assetWizardSettings.xml +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser deleted file mode 100644 index 3f6cd0f..0000000 Binary files a/.idea/caches/build_file_checksums.ser and /dev/null differ diff --git a/.idea/caches/gradle_models.ser b/.idea/caches/gradle_models.ser deleted file mode 100644 index 5971202..0000000 Binary files a/.idea/caches/gradle_models.ser and /dev/null differ diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml deleted file mode 100644 index ae78c11..0000000 --- a/.idea/codeStyles/Project.xml +++ /dev/null @@ -1,113 +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/compiler.xml b/.idea/compiler.xml deleted file mode 100644 index b589d56..0000000 --- a/.idea/compiler.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml deleted file mode 100644 index 1431050..0000000 --- a/.idea/gradle.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml deleted file mode 100644 index eb2873e..0000000 --- a/.idea/jarRepositories.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fdf8d99..b1077fb 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 5ac489c..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 8c565d6..0000000 --- a/.idea/modules.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 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7a118b4 --- /dev/null +++ b/Gemfile @@ -0,0 +1,3 @@ +source "https://rubygems.org" + +gem "fastlane" diff --git a/app/build.gradle b/app/build.gradle index 33df36f..19592a8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,23 +1,40 @@ apply plugin: 'com.android.application' apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'kotlin-kapt' +def relStorePassword = System.getenv("RELEASE_STORE_PASSWORD") +def relKeyPassword = System.getenv("RELEASE_KEY_PASSWORD") +def relKeyAlias = System.getenv("RELEASE_KEY_ALIAS") + +def keystorePath = "/keystore.jks" +def keystore = file(keystorePath).exists() ? file(keystorePath) : null android { compileSdkVersion 31 defaultConfig { applicationId "com.appttude.h_mal.farmr" minSdkVersion 21 targetSdkVersion 31 - versionCode 1 - versionName "1.0" - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' + versionCode 2 + versionName "2.0" + testInstrumentationRunner 'com.appttude.h_mal.farmr.application.TestRunner' vectorDrawables.useSupportLibrary = true } + signingConfigs { + release { + storePassword relStorePassword + keyPassword relKeyPassword + keyAlias relKeyAlias + storeFile keystore + } + } buildTypes { release { + signingConfig signingConfigs.release minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + useLibrary 'android.test.mock' } dependencies { @@ -29,8 +46,46 @@ dependencies { implementation 'androidx.appcompat:appcompat:1.0.0' implementation 'com.google.android.material:material:1.0.0' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.fragment:fragment-ktx:1.4.0' + implementation 'androidx.activity:activity-ktx:1.4.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' + implementation 'androidx.preference:preference:1.2.1' implementation 'com.ajts.androidmads.SQLite2Excel:library:1.0.2' - testImplementation 'junit:junit:4.12' + / * Unit testing * / + testImplementation 'junit:junit:4.13.2' + androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" + implementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version" androidTestImplementation 'androidx.test:core-ktx:1.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' + / * mockito and livedata testing * / + testImplementation 'org.mockito:mockito-inline:2.13.0' + testImplementation 'androidx.arch.core:core-testing:2.1.0' + / * MockK * / + def mockk_ver = "1.10.5" + testImplementation "io.mockk:mockk:$mockk_ver" + androidTestImplementation "io.mockk:mockk-android:$mockk_ver" + / * Android Espresso * / + def testJunitVersion = "1.1.5" + def testRunnerVersion = "1.5.2" + def espressoVersion = "3.5.1" + androidTestImplementation "androidx.test.ext:junit:$testJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion" + androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espressoVersion" + implementation "androidx.test.espresso:espresso-idling-resource:$espressoVersion" + androidTestImplementation "androidx.test:runner:$testRunnerVersion" + androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion" + androidTestImplementation "androidx.test.espresso:espresso-intents:$espressoVersion" + androidTestImplementation "org.hamcrest:hamcrest:2.2" + / * Room database * / + def room_version = "2.4.3" + implementation "androidx.room:room-runtime:$room_version" + kapt "androidx.room:room-compiler:$room_version" + implementation "androidx.room:room-ktx:$room_version" + / *Kodein Dependency Injection * / + def kodein_version = "6.2.1" + implementation "org.kodein.di:kodein-di-generic-jvm:$kodein_version" + implementation "org.kodein.di:kodein-di-framework-android-x:$kodein_version" + / * jxl * / + implementation 'net.sourceforge.jexcelapi:jxl:2.6.12' } diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt new file mode 100644 index 0000000..616c325 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestAppClass.kt @@ -0,0 +1,38 @@ +package com.appttude.h_mal.farmr.application + +import androidx.test.espresso.IdlingRegistry +import androidx.test.espresso.idling.CountingIdlingResource +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.base.BaseApplication +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.model.Shift + +class TestAppClass : BaseApplication() { + private val idlingResources = CountingIdlingResource("Data_loader") + + lateinit var database: LegacyDatabase + lateinit var preferenceProvider: PreferenceProvider + + override fun onCreate() { + super.onCreate() + IdlingRegistry.getInstance().register(idlingResources) + } + + override fun createDatabase(): LegacyDatabase { + database = + LegacyDatabase(InstrumentationRegistry.getInstrumentation().context.contentResolver) + return database + } + + override fun createPrefs(): PreferenceProvider { + preferenceProvider = PreferenceProvider(this) + return preferenceProvider + } + + fun addToDatabase(shift: Shift) = database.insertShiftDataIntoDatabase(shift) + fun addShiftsToDatabase(shifts: List) = shifts.forEach { addToDatabase(it) } + fun clearDatabase() = database.deleteAllShiftsInDatabase() + fun cleanPrefs() = preferenceProvider.clearPrefs() + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt new file mode 100644 index 0000000..ee712d4 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/application/TestRunner.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.application + +import android.app.Application +import android.content.Context +import androidx.test.runner.AndroidJUnitRunner + +class TestRunner : AndroidJUnitRunner() { + @Throws( + InstantiationException::class, + IllegalAccessException::class, + ClassNotFoundException::class + ) + override fun newApplication( + cl: ClassLoader?, + className: String?, + context: Context? + ): Application { + return super.newApplication(cl, TestAppClass::class.java.name, context) + } + +} diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt index d8872a5..31f500e 100644 --- a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/ShiftProviderTest.kt @@ -3,21 +3,23 @@ package com.appttude.h_mal.farmr.data import android.content.ContentResolver import android.content.ContentValues import androidx.test.rule.provider.ProviderTestRule -import com.appttude.h_mal.farmr.data.ShiftsContract.CONTENT_AUTHORITY -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry.CONTENT_URI -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.data.legacydb.ShiftProvider +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.CONTENT_AUTHORITY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID import junit.framework.TestCase.assertEquals import junit.framework.TestCase.assertNull +import org.junit.After import org.junit.Rule import org.junit.Test @@ -30,6 +32,11 @@ class ShiftProviderTest { private val contentResolver: ContentResolver get() = providerRule.resolver + @After + fun tearDown() { + contentResolver.delete(CONTENT_URI, null, null) + } + @Test fun insertEntry_queryEntry_assertEntry() { // Arrange @@ -75,6 +82,8 @@ class ShiftProviderTest { // Assert val item = contentResolver.query(CONTENT_URI, projection, null, null, null) item?.takeIf { it.moveToNext() }?.run { + val id = getLong(getColumnIndexOrThrow(_ID)) + val descriptionColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DESCRIPTION)) val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE)) val timeInColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN)) diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt new file mode 100644 index 0000000..e539bdd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabaseTest.kt @@ -0,0 +1,91 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import androidx.test.rule.provider.ProviderTestRule +import com.appttude.h_mal.farmr.model.Shift +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class LegacyDatabaseTest { + @get:Rule + val providerRule: ProviderTestRule = ProviderTestRule + .Builder(ShiftProvider::class.java, ShiftsContract.CONTENT_AUTHORITY) + .build() + + private lateinit var database: LegacyDatabase + + @Before + fun setup() { + database = LegacyDatabase(providerRule.resolver) + } + + @After + fun tearDown() { + database.deleteAllShiftsInDatabase() + } + + @Test + fun insertShift_readShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val retrievedShift = database.readShiftsFromDatabase()?.first() + + // Assert + assertEquals(retrievedShift?.description, shift.description) + assertEquals(retrievedShift?.date, shift.date) + assertEquals(retrievedShift?.units, shift.units) + assertEquals(retrievedShift?.rateOfPay, shift.rateOfPay) + } + + @Test + fun insertShift_updateShift_successfulRead() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.updateShiftDataIntoDatabase( + id = id, + typeString = updateShift.type.type, + descriptionString = updateShift.description, + dateString = updateShift.date, + timeInString = updateShift.timeIn ?: "", + timeOutString = updateShift.timeOut ?: "", + duration = updateShift.duration ?: 0f, + breaks = updateShift.breakMins ?: 0, + units = updateShift.units!!, + payRate = updateShift.rateOfPay, + totalPay = updateShift.totalPay + ) + val retrievedShift = database.readSingleShiftWithId(id) + + // Assert + assertEquals(retrievedShift?.description, updateShift.description) + assertEquals(retrievedShift?.date, updateShift.date) + assertEquals(retrievedShift?.units, updateShift.units) + assertEquals(retrievedShift?.rateOfPay, updateShift.rateOfPay) + } + + @Test + fun insertShift_deleteShift_databaseEmpty() { + // Arrange + val shift = Shift("adsfadsf", "2020-12-12", 12f, 12f) + val updateShift = Shift("dasdads", "2020-11-12", 10f, 10f) + + // Act + database.insertShiftDataIntoDatabase(shift) + database.insertShiftDataIntoDatabase(updateShift) + val id = database.readShiftsFromDatabase()?.first()!!.id + database.deleteSingleShift(id) + + // Assert + assertEquals(database.readShiftsFromDatabase()?.size, 1) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt new file mode 100644 index 0000000..8d4ebb8 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTest.kt @@ -0,0 +1,107 @@ +package com.appttude.h_mal.farmr.ui + +import android.Manifest +import android.app.Activity +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.view.View +import androidx.test.core.app.ActivityScenario +import androidx.test.espresso.Espresso +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.assertion.ViewAssertions +import androidx.test.espresso.matcher.RootMatchers.withDecorView +import androidx.test.espresso.matcher.ViewMatchers +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.GrantPermissionRule +import com.appttude.h_mal.farmr.application.TestAppClass +import com.appttude.h_mal.farmr.ui.utils.getShifts +import kotlinx.coroutines.runBlocking +import org.hamcrest.Matcher +import org.hamcrest.Matchers +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.kodein.di.android.kodein + +@Suppress("EmptyMethod") +open class BaseTest( + private val activity: Class, + private val intentBundle: Bundle? = null, +) { + + lateinit var scenario: ActivityScenario + private lateinit var testApp: TestAppClass + private lateinit var testActivity: Activity + private lateinit var decorView: View + + @get:Rule + var permissionRule = GrantPermissionRule.grant(Manifest.permission.READ_EXTERNAL_STORAGE) + + @Before + open fun setUp() { + val startIntent = + Intent(InstrumentationRegistry.getInstrumentation().targetContext, activity) + if (intentBundle != null) { + startIntent.replaceExtras(intentBundle) + } + + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + kodein(testApp) + runBlocking { + beforeLaunch() + } + + scenario = ActivityScenario.launch(startIntent) + scenario.onActivity { + testApp = + InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as TestAppClass + onLaunch() + decorView = it.window.decorView + testActivity = it + } + afterLaunch() + } + + fun getActivity() = testActivity + + @After + fun tearDown() { + testFinished() + } + + open fun beforeLaunch() {} + open fun onLaunch() {} + open fun afterLaunch() {} + open fun testFinished() {} + + fun waitFor(delay: Long) { + Espresso.onView(ViewMatchers.isRoot()).perform(object : ViewAction { + override fun getConstraints(): Matcher = ViewMatchers.isRoot() + override fun getDescription(): String = "wait for $delay milliseconds" + override fun perform(uiController: UiController, v: View?) { + uiController.loopMainThreadForAtLeast(delay) + } + }) + } + + @Suppress("DEPRECATION") + fun checkToastMessage(message: String) { + Espresso.onView(ViewMatchers.withText(message)).inRoot(withDecorView(Matchers.not(decorView))) + .check(ViewAssertions.matches(ViewMatchers.isDisplayed())) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + waitFor(3500) + } + } + + fun navigateBack() = Espresso.pressBack() + + fun addRandomShifts() { + testApp.addShiftsToDatabase(getShifts()) + } + + fun clearDataBase() = testApp.clearDatabase() + fun clearPrefs() = testApp.cleanPrefs() +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt new file mode 100644 index 0000000..a85098f --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/BaseTestRobot.kt @@ -0,0 +1,219 @@ +package com.appttude.h_mal.farmr.ui + +import android.content.res.Resources +import android.view.View +import android.widget.DatePicker +import android.widget.TimePicker +import androidx.annotation.StringRes +import androidx.appcompat.widget.AppCompatButton +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import androidx.test.espresso.Espresso.onData +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.openActionBarOverflowOrOptionsMenu +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.action.ViewActions +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.swipeDown +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.PickerActions +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.matcher.RootMatchers.isDialog +import androidx.test.espresso.matcher.ViewMatchers.* +import androidx.test.platform.app.InstrumentationRegistry +import com.appttude.h_mal.farmr.ui.utils.EspressoHelper.waitForView +import org.hamcrest.CoreMatchers.allOf +import org.hamcrest.CoreMatchers.anything +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.Matcher +import org.hamcrest.Matchers + +@SuppressWarnings("unused") +open class BaseTestRobot { + + fun fillEditText(resId: Int, text: String?): ViewInteraction = + onView(withId(resId)).perform( + ViewActions.replaceText(text), + ViewActions.closeSoftKeyboard() + ) + + fun clickButton(resId: Int): ViewInteraction = + onView((withId(resId))).perform(click()) + +// fun clickMenu(menuId: Int): ViewInteraction = onView() + + fun matchView(resId: Int): ViewInteraction = onView(withId(resId)) + + fun matchViewWaitFor(resId: Int): ViewInteraction = waitForView(withId(resId)) + + fun matchDisplayed(resId: Int): ViewInteraction = matchView(resId).check(matches(isDisplayed())) + + fun matchText(viewInteraction: ViewInteraction, text: String): ViewInteraction = viewInteraction + .check(matches(withText(text))) + + fun matchText(viewId: Int, textId: Int): ViewInteraction = onView(withId(viewId)) + .check(matches(withText(textId))) + + fun matchText(resId: Int, text: String): ViewInteraction = matchText(matchView(resId), text) + + fun clickListItem(listRes: Int, position: Int) { + onData(anything()) + .inAdapterView(allOf(withId(listRes))) + .atPosition(position).perform(click()) + } + + fun clickOnMenuItem(menuId: Int) { + openActionBarOverflowOrOptionsMenu(InstrumentationRegistry.getInstrumentation().context) + onView(withText(menuId)).perform(click()) + } + + fun clickDialogButton(text: String) { + onView(withText(text)).inRoot(isDialog()) + .check(matches(isDisplayed())) + .perform(click()); + } + + fun scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ) + ) + } + + fun scrollToRecyclerItem( + recyclerId: Int, + resIdForString: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(resIdForString)) + ) + ) + } + + fun scrollToRecyclerItemByPosition( + recyclerId: Int, + position: Int + ): ViewInteraction? { + return matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position) + ) + } + + fun clickViewInRecycler(recyclerId: Int, resIdForString: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.actionOnItem( + hasDescendant(withText(resIdForString)), + click() + ) + ) + } + + fun clickRecyclerAtPosition(recyclerId: Int, position: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, click()), + ) + } + + fun clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) { + matchView(recyclerId) + .perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollToPosition(position), + RecyclerViewActions.actionOnItemAtPosition(position, object : ViewAction { + override fun getDescription(): String { + return "click on subview in RecyclerView at position: $position" + } + + override fun getConstraints(): Matcher { + return Matchers.allOf( + isAssignableFrom( + RecyclerView::class.java + ), isDisplayed() + ) + } + + override fun perform(uiController: UiController?, view: View?) { + view?.findViewById(subViewId)?.performClick() + } + + }), + ) + } + + fun clickOnRecyclerItemWithText(recyclerId: Int, text: String) { + matchView(recyclerId).perform( + // scrollTo will fail the test if no item matches. + RecyclerViewActions.scrollTo( + hasDescendant(withText(text)) + ), + RecyclerViewActions.actionOnItem( + hasDescendant(withText(text)), + click() + ) + ) + } + + fun swipeDown(resId: Int): ViewInteraction = + onView(withId(resId)).perform(swipeDown()) + + fun getStringFromResource(@StringRes resId: Int): String = + Resources.getSystem().getString(resId) + + fun pullToRefresh(resId: Int) { + onView(allOf(withId(resId), isDisplayed())).perform(swipeDown()) + } + + fun selectDateInPicker(year: Int, month: Int, day: Int) { + onView(withClassName(equalTo(DatePicker::class.java.name))).perform( + PickerActions.setDate( + year, + month, + day + ) + ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) + } + + fun selectTextInSpinner(id: Int, text: String) { + clickButton(id) + onView(withSpinnerText(text)).perform(click()) + } + + fun selectTimeInPicker(hours: Int, minutes: Int) { + onView(withClassName(equalTo(TimePicker::class.java.name))).perform( + PickerActions.setTime( + hours, minutes + ) + ) + onView( + allOf( + withClassName(equalTo(AppCompatButton::class.java.name)), + withText("OK") + ) + ).perform( + click() + ) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt new file mode 100644 index 0000000..c28cfb1 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/AddItemScreenRobot.kt @@ -0,0 +1,41 @@ +package com.appttude.h_mal.farmr.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun addScreen(func: AddItemScreenRobot.() -> Unit) = AddItemScreenRobot().apply { func() } +class AddItemScreenRobot : BaseTestRobot() { + + fun clickShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> clickButton(R.id.hourly) + ShiftType.PIECE -> clickButton(R.id.piecerate) + } + } + + fun setDescription(text: String?) = fillEditText(R.id.locationEditText, text) + fun setDate(year: Int, month: Int, day: Int) { + clickButton(R.id.dateEditText) + selectDateInPicker(year, month, day) + } + + fun setTimeIn(hour: Int, minutes: Int) { + clickButton(R.id.timeInEditText) + selectTimeInPicker(hour, minutes) + } + + fun setTimeOut(hour: Int, minutes: Int) { + clickButton(R.id.timeOutEditText) + selectTimeInPicker(hour, minutes) + } + + fun setBreakTime(mins: Int) = fillEditText(R.id.breakEditText, mins.toString()) + fun setUnits(units: Float) = fillEditText(R.id.unitET, units.toString()) + fun setRateOfPay(rateOfPay: Float) = fillEditText(R.id.payrateET, rateOfPay.toString()) + fun submit() = clickButton(R.id.submit) + + fun assertTotalPay(pay: String) = matchText(R.id.totalpayval, pay) + fun assertDuration(duration: String) = matchText(R.id.ShiftDuration, duration) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt new file mode 100644 index 0000000..acd6f63 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/FilterScreenRobot.kt @@ -0,0 +1,29 @@ +package com.appttude.h_mal.farmr.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun filterScreen(func: FilterScreenRobot.() -> Unit) = FilterScreenRobot().apply { func() } +class FilterScreenRobot : BaseTestRobot() { + + fun setDescription(text: String?) = fillEditText(R.id.filterLocationEditText, text) + + fun setDateIn(year: Int, month: Int, day: Int) { + clickButton(R.id.fromdateInEditText) + selectDateInPicker(year, month, day) + } + + fun setDateOut(year: Int, month: Int, day: Int) { + clickButton(R.id.filterDateOutEditText) + selectDateInPicker(year, month, day) + } + + fun setType(type: ShiftType?) = when(type) { + ShiftType.HOURLY -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + ShiftType.PIECE -> selectTextInSpinner(R.id.TypeFilterEditText, type.type) + null -> selectTextInSpinner(R.id.TypeFilterEditText, "") + } + fun submit() = clickButton(R.id.submitFiltered) + +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt new file mode 100644 index 0000000..81bf480 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/HomeScreenRobot.kt @@ -0,0 +1,28 @@ +package com.appttude.h_mal.farmr.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter.CurrentViewHolder +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun homeScreen(func: HomeScreenRobot.() -> Unit) = HomeScreenRobot().apply { func() } +class HomeScreenRobot : BaseTestRobot() { + + fun clickOnItemWithText(text: String) = clickOnRecyclerItemWithText(R.id.list_item_view, text) + fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition(R.id.list_item_view, position) + fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition(R.id.list_item_view, position, R.id.imageView) + fun clickFab() = clickButton(R.id.fab1) + fun clickOnInfoIcon() = clickButton(R.id.action_favorite) + fun clickFilterInMenu() = clickOnMenuItem(R.string.filter) + fun clickClearFilterInMenu() = clickOnMenuItem(R.string.clear) + fun clickSortInMenu() = clickOnMenuItem(R.string.sort) + + fun applySort(sortable: Sortable, order: Order = Order.ASCENDING) { + clickSortInMenu() + val label = sortable.label + clickDialogButton(label) + val orderLabel = order.label + clickDialogButton(orderLabel) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt new file mode 100644 index 0000000..b56d799 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/robots/ViewItemScreenRobot.kt @@ -0,0 +1,33 @@ +package com.appttude.h_mal.farmr.ui.robots + +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.ui.BaseTestRobot + +fun viewScreen(func: ViewItemScreenRobot.() -> Unit) = ViewItemScreenRobot().apply { func() } +class ViewItemScreenRobot : BaseTestRobot() { + + fun matchShiftType(type: ShiftType) { + when (type) { + ShiftType.HOURLY -> matchText(R.id.details_shift, type.type) + ShiftType.PIECE -> matchText(R.id.details_shift, type.type) + } + } + + fun matchDescription(text: String) = matchText(R.id.details_desc, text) + fun matchDate(date: String) { + matchText(R.id.details_date, date) + } + + fun matchTime(timeIn: String, timeOut: String) { + matchText(R.id.details_time, "$timeIn-$timeOut") + } + + fun matchBreakTime(mins: Int) = matchText(R.id.details_breaks, mins.toString()) + fun matchUnits(units: Float) = fillEditText(R.id.details_units, units.toString()) + fun matchRateOfPay(rateOfPay: Float) = fillEditText(R.id.details_pay_rate, rateOfPay.toString()) + fun matchTotalPay(pay: String) = matchText(R.id.details_totalpay, pay) + fun matchDuration(duration: String) = matchText(R.id.details_duration, duration) + + fun clickEdit() = clickButton(R.id.details_edit) +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt new file mode 100644 index 0000000..a7decde --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/tests/ShiftTests.kt @@ -0,0 +1,141 @@ +package com.appttude.h_mal.farmr.ui.tests + +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.ui.BaseTest +import com.appttude.h_mal.farmr.ui.MainActivity +import com.appttude.h_mal.farmr.ui.robots.addScreen +import com.appttude.h_mal.farmr.ui.robots.filterScreen +import com.appttude.h_mal.farmr.ui.robots.homeScreen +import com.appttude.h_mal.farmr.ui.robots.viewScreen +import org.junit.Test + +class ShiftTests : BaseTest(MainActivity::class.java) { + + override fun afterLaunch() { + super.afterLaunch() + addRandomShifts() + + // Content resolver hard to mock + // Dirty technique to have a populated list + homeScreen { + clickFab() + navigateBack() + } + } + + override fun testFinished() { + super.testFinished() + clearDataBase() + clearPrefs() + } + + // Add a shift successfully + @Test + fun openAddScreen_addNewShift_newShiftCreated() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.HOURLY) + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(10f) + assertDuration("2.0 hours") + assertTotalPay("£20.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Edit a shift successfully + @Test + fun test2() { + homeScreen { + clickOnEdit(0) + } + addScreen { + setDescription("Edited this shift") + setTimeIn(12, 0) + setTimeOut(14, 30) + setBreakTime(30) + setRateOfPay(20f) + assertDuration("2.0 hours") + assertTotalPay("£40.00") + submit() + } + homeScreen { + clickOnItemWithText("Edited this shift") + } + viewScreen { + matchDescription("Edited this shift") + matchDuration("2 Hours 0 Minutes (+ 30 minutes break)") + matchTotalPay("2.0 Hours @ £20.00 per Hour\nEquals: £40.00") + } + } + + // filter the list with date from + @Test + fun test3() { + homeScreen { + applySort(Sortable.TYPE, Order.DESCENDING) + clickOnItemAtPosition(0) + viewScreen { + matchDescription("Day five") + matchShiftType(ShiftType.PIECE) + } + } + } + + // filter the list with date to + @Test + fun test4() { + homeScreen { + clickFilterInMenu() + } + filterScreen { + setDateIn(2023,8,3) + setDateOut(2023,8,6) + submit() + } + homeScreen { + clickOnItemAtPosition(0) + } + } + + // Add a shift as piece rate + @Test + fun test5() { + homeScreen { + clickFab() + } + addScreen { + setDescription("This is a description") + setDate(2023, 2, 11) + clickShiftType(ShiftType.PIECE) + setRateOfPay(10f) + setUnits(1f) + assertTotalPay("£10.00") + submit() + } + homeScreen { + clickOnItemWithText("This is a description") + } + } + + // Validate the details screen + @Test + fun test6() { + } + + // filter, sort, order and then reset + @Test + fun test7() { + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt new file mode 100644 index 0000000..ad69acd --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/Constants.kt @@ -0,0 +1 @@ +package com.appttude.h_mal.farmr.ui.utils diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt new file mode 100644 index 0000000..0e05bb6 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/DataHelper.kt @@ -0,0 +1,103 @@ +package com.appttude.h_mal.farmr.ui.utils + +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType + +fun getShifts() = listOf( + Shift( + ShiftType.HOURLY, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + 0, + 0f, + 10f, + 10f + ), + Shift( + ShiftType.HOURLY, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.HOURLY, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + 0f, + 10f, + 5f + ), + Shift( + ShiftType.PIECE, + "Day five", + "2023-08-05", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day six", + "2023-08-06", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day seven", + "2023-08-07", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ), + Shift( + ShiftType.PIECE, + "Day eight", + "2023-08-08", + "", + "", + 0f, + 0, + 1f, + 10f, + 10f + ) +) \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt new file mode 100644 index 0000000..a2ebc7a --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/EspressoHelper.kt @@ -0,0 +1,123 @@ +package com.appttude.h_mal.farmr.ui.utils + +import android.os.SystemClock.sleep +import android.view.View +import android.widget.CheckBox +import android.widget.Checkable +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.NoMatchingViewException +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import androidx.test.espresso.ViewInteraction +import androidx.test.espresso.matcher.ViewMatchers.isRoot +import androidx.test.espresso.util.TreeIterables +import org.hamcrest.BaseMatcher +import org.hamcrest.CoreMatchers.isA +import org.hamcrest.Description +import org.hamcrest.Matcher + + +object EspressoHelper { + + /** + * Perform action of waiting for a certain view within a single root view + * @param viewMatcher Generic Matcher used to find our view + */ + fun searchFor(viewMatcher: Matcher): ViewAction { + + return object : ViewAction { + + override fun getConstraints(): Matcher = isRoot() + override fun getDescription(): String { + return "searching for view $this in the root view" + } + + override fun perform(uiController: UiController, view: View) { + var tries = 0 + val childViews: Iterable = TreeIterables.breadthFirstViewTraversal(view) + + // Look for the match in the tree of child views + childViews.forEach { + tries++ + if (viewMatcher.matches(it)) { + // found the view + return + } + } + + throw NoMatchingViewException.Builder() + .withRootView(view) + .withViewMatcher(viewMatcher) + .build() + } + } + } + + /** + * Performs an action to check/uncheck a checkbox + * + */ + fun setChecked(checked: Boolean): ViewAction { + return object : ViewAction { + override fun getConstraints(): BaseMatcher { + return object : BaseMatcher() { + override fun describeTo(description: Description?) {} + + override fun matches(actual: Any?): Boolean { + return isA(CheckBox::class.java).matches(actual) + } + } + } + + override fun getDescription(): String { + return "" + } + + override fun perform(uiController: UiController, view: View) { + val checkableView = view as Checkable + checkableView.isChecked = checked + } + } + } + + /** + * Perform action of implicitly waiting for a certain view. + * This differs from EspressoExtensions.searchFor in that, + * upon failure to locate an element, it will fetch a new root view + * in which to traverse searching for our @param match + * + * @param viewMatcher ViewMatcher used to find our view + */ + fun waitForView( + viewMatcher: Matcher, + waitMillis: Int = 5000, + waitMillisPerTry: Long = 100 + ): ViewInteraction { + + // Derive the max tries + val maxTries = waitMillis / waitMillisPerTry.toInt() + + var tries = 0 + + for (i in 0..maxTries) + try { + // Track the amount of times we've tried + tries++ + + // Search the root for the view + onView(isRoot()).perform(searchFor(viewMatcher)) + + // If we're here, we found our view. Now return it + return onView(viewMatcher) + + } catch (e: Exception) { + + if (tries == maxTries) { + throw e + } + sleep(waitMillisPerTry) + } + + throw Exception("Error finding a view matching $viewMatcher") + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt new file mode 100644 index 0000000..d398697 --- /dev/null +++ b/app/src/androidTest/java/com/appttude/h_mal/farmr/ui/utils/TestUtils.kt @@ -0,0 +1,32 @@ +package com.appttude.h_mal.farmr.ui.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8dc1d6f..6fc2773 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + + + + \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt deleted file mode 100644 index 78945fb..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FilterDataFragment.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.DatePickerDialog -import android.os.Bundle -import android.text.TextUtils -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.Button -import android.widget.EditText -import android.widget.Spinner -import androidx.fragment.app.Fragment -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import java.text.MessageFormat -import java.text.SimpleDateFormat -import java.util.Calendar -import java.util.Date - -class FilterDataFragment : Fragment() { - private val spinnerList: Array = arrayOf("", "Hourly", "Piece Rate") - private val listArgs: MutableList = ArrayList() - private var LocationET: EditText? = null - private var dateFromET: EditText? = null - private var dateToET: EditText? = null - private var typeSpinner: Spinner? = null - lateinit var mcurrentDate: Calendar - private lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - val activity = (activity) - - // Inflate the layout for this fragment - val rootView: View = inflater.inflate(R.layout.fragment_filter_data, container, false) - activity.setActionBarTitle(getString(R.string.title_activity_filter_data)) - mcurrentDate = Calendar.getInstance() - LocationET = rootView.findViewById(R.id.filterLocationEditText) as EditText? - dateFromET = rootView.findViewById(R.id.fromdateInEditText) as EditText? - dateToET = rootView.findViewById(R.id.filterDateOutEditText) as EditText? - typeSpinner = rootView.findViewById(R.id.TypeFilterEditText) as Spinner? - - - if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?")) { - var str: String = activity.args!!.get(2) - str = str.replace("%", "") - LocationET!!.setText(str) - } - if (activity.selection != null && !(activity.args!!.get(0) == "2000-01-01")) { - dateFromET!!.setText(activity.args!!.get(0)) - } - if ((activity.selection != null) && (activity.args != null) && !(activity.args!![1] == (mcurrentDate.get(Calendar.YEAR).toString() + "-" - + String.format("%02d", (mcurrentDate.get(Calendar.MONTH) + 1)) + "-" - + String.format("%02d", mcurrentDate.get(Calendar.DAY_OF_MONTH))).toString())) { - dateToET!!.setText(activity.args!![1]) - } - dateFromET!!.setOnClickListener { //To show current date in the datepicker - var mYear: Int = mcurrentDate.get(Calendar.YEAR) - var mMonth: Int = mcurrentDate.get(Calendar.MONTH) - var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH) - if (!(dateFromET!!.text.toString() == "")) { - val dateString: String = dateFromET!!.text.toString().trim() - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth = mMonth - 1 - } - mDay = dateString.substring(8).toInt() - } - - val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday -> - val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday)) - dateFromET!!.setText(input) - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - dateToET!!.setOnClickListener { //To show current date in the datepicker - val mcurrentDate: Calendar = Calendar.getInstance() - var mYear: Int = mcurrentDate.get(Calendar.YEAR) - var mMonth: Int = mcurrentDate.get(Calendar.MONTH) - var mDay: Int = mcurrentDate.get(Calendar.DAY_OF_MONTH) - if (!(dateToET!!.text.toString() == "")) { - val dateString: String = dateToET!!.text.toString().trim({ it <= ' ' }) - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth -= 1 - } - mDay = dateString.substring(8).toInt() - } - val mDatePicker = DatePickerDialog((context)!!, { datepicker, selectedyear, selectedmonth, selectedday -> - val input = MessageFormat.format("{0}-{1}-{2}", selectedyear, String.format("%02d", (selectedmonth + 1)), String.format("%02d", selectedday)) - dateToET!!.setText(input) - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - val adapter: ArrayAdapter = ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList) - typeSpinner!!.adapter = adapter - if (activity.selection != null && activity.selection!!.contains(" AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?")) { - val spinnerPosition: Int = adapter.getPosition(activity.args!!.get(activity.args!!.size - 1)) - typeSpinner!!.setSelection(spinnerPosition) - } - val submit: Button = rootView.findViewById(R.id.submitFiltered) as Button - submit.setOnClickListener { - BuildQuery() - activity.args = listArgs.toTypedArray() - FragmentMain.NEW_LOADER = 1 - activity.fragmentManager!!.popBackStack() - } - return rootView - } - - private fun BuildQuery() { - val dateQuery: String = ShiftsEntry.COLUMN_SHIFT_DATE + " BETWEEN ? AND ?" - val descQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_DESCRIPTION + " LIKE ?" - val typeQuery: String = " AND " + ShiftsEntry.COLUMN_SHIFT_TYPE + " IS ?" - var dateFrom = "2000-01-01" - val c: Date = Calendar.getInstance().time - val df = SimpleDateFormat("yyyy-MM-dd") - var dateTo: String = df.format(c) - if (!TextUtils.isEmpty(dateFromET!!.text.toString().trim())) { - dateFrom = dateFromET!!.text.toString().trim() - } - if (!TextUtils.isEmpty(dateToET!!.text.toString().trim())) { - dateTo = dateToET!!.text.toString().trim() - } - activity.selection = dateQuery - listArgs.add(dateFrom) - listArgs.add(dateTo) - if (!TextUtils.isEmpty(LocationET!!.text.toString().trim())) { - activity.selection = activity.selection + descQuery - listArgs.add("%" + LocationET!!.text.toString().trim() + "%") - } - if (!(typeSpinner!!.selectedItem.toString() == "")) { - activity.selection = activity.selection + typeQuery - listArgs.add(typeSpinner!!.selectedItem.toString()) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt deleted file mode 100644 index eedcee5..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentAddItem.kt +++ /dev/null @@ -1,507 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.DatePickerDialog -import android.app.DatePickerDialog.OnDateSetListener -import android.app.TimePickerDialog -import android.app.TimePickerDialog.OnTimeSetListener -import android.content.ContentValues -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.text.Editable -import android.text.TextUtils -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.DatePicker -import android.widget.EditText -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.RadioButton -import android.widget.RadioGroup -import android.widget.ScrollView -import android.widget.TextView -import android.widget.TimePicker -import android.widget.Toast -import androidx.fragment.app.Fragment -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import java.util.Calendar - -class FragmentAddItem : Fragment(), LoaderManager.LoaderCallbacks { - lateinit var activity: MainActivity - - private var mCurrentProductUri: Uri? = null - private var mRadioButtonOne: RadioButton? = null - private var mRadioButtonTwo: RadioButton? = null - private var mLocationEditText: EditText? = null - private var mDateEditText: EditText? = null - private var mDurationTextView: TextView? = null - private var mTimeInEditText: EditText? = null - private var mTimeOutEditText: EditText? = null - private var mBreakEditText: EditText? = null - private var mUnitEditText: EditText? = null - private var mPayRateEditText: EditText? = null - private var mTotalPayTextView: TextView? = null - private var hourlyDataView: LinearLayout? = null - private var unitsHolder: LinearLayout? = null - private var durationHolder: LinearLayout? = null - private var wholeView: LinearLayout? = null - private var scrollView: ScrollView? = null - private var progressBarAI: ProgressBar? = null - private var mBreaks = 0 - private var mDay = 0 - private var mMonth = 0 - private var mYear = 0 - private var mHoursIn = 0 - private var mMinutesIn = 0 - private var mHoursOut = 0 - private var mMinutesOut = 0 - private var mUnits = 0f - private var mPayRate = 0f - private var mType: String? = null - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - val rootView = inflater.inflate(R.layout.fragment_add_item, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - - progressBarAI = rootView.findViewById(R.id.pd_ai) as ProgressBar - scrollView = rootView.findViewById(R.id.total_view) as ScrollView - mRadioGroup = rootView.findViewById(R.id.rg) as RadioGroup - mRadioButtonOne = rootView.findViewById(R.id.hourly) as RadioButton - mRadioButtonTwo = rootView.findViewById(R.id.piecerate) as RadioButton - mLocationEditText = rootView.findViewById(R.id.locationEditText) as EditText - mDateEditText = rootView.findViewById(R.id.dateEditText) as EditText - mTimeInEditText = rootView.findViewById(R.id.timeInEditText) as EditText - mBreakEditText = rootView.findViewById(R.id.breakEditText) as EditText - mTimeOutEditText = rootView.findViewById(R.id.timeOutEditText) as EditText - mDurationTextView = rootView.findViewById(R.id.ShiftDuration) as TextView - mUnitEditText = rootView.findViewById(R.id.unitET) as EditText - mPayRateEditText = rootView.findViewById(R.id.payrateET) as EditText - mTotalPayTextView = rootView.findViewById(R.id.totalpayval) as TextView - hourlyDataView = rootView.findViewById(R.id.hourly_data_holder) as LinearLayout - unitsHolder = rootView.findViewById(R.id.units_holder) as LinearLayout - durationHolder = rootView.findViewById(R.id.duration_holder) as LinearLayout - wholeView = rootView.findViewById(R.id.whole_view) as LinearLayout - mPayRate = 0.0f - mUnits = 0.0f - val b = arguments - if (b != null) { - mCurrentProductUri = Uri.parse(b.getString("uri")) - } - if (mCurrentProductUri == null) { - activity.setActionBarTitle(getString(R.string.add_item_title)) - wholeView!!.visibility = View.GONE - } else { - activity.setActionBarTitle(getString(R.string.edit_item_title)) - loaderManager.initLoader(EXISTING_PRODUCT_LOADER, null, this) - } - mBreakEditText!!.hint = "insert break in minutes" - mRadioGroup!!.setOnCheckedChangeListener(RadioGroup.OnCheckedChangeListener { radioGroup, i -> - if (mRadioButtonOne!!.isChecked) { - mType = mRadioButtonOne!!.text.toString() - wholeView!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.GONE - hourlyDataView!!.visibility = View.VISIBLE - durationHolder!!.visibility = View.VISIBLE - } else if (mRadioButtonTwo!!.isChecked) { - mType = mRadioButtonTwo!!.text.toString() - wholeView!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.VISIBLE - hourlyDataView!!.visibility = View.GONE - durationHolder!!.visibility = View.GONE - } - }) - mDateEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - //To show current date in the datepicker - if (TextUtils.isEmpty(mDateEditText!!.text.toString().trim { it <= ' ' })) { - val mcurrentDate = Calendar.getInstance() - mYear = mcurrentDate[Calendar.YEAR] - mMonth = mcurrentDate[Calendar.MONTH] - mDay = mcurrentDate[Calendar.DAY_OF_MONTH] - } else { - val dateString = mDateEditText!!.text.toString().trim { it <= ' ' } - mYear = dateString.substring(0, 4).toInt() - mMonth = dateString.substring(5, 7).toInt() - if (mMonth == 1) { - mMonth = 0 - } else { - mMonth = mMonth - 1 - } - mDay = dateString.substring(8).toInt() - } - val mDatePicker = DatePickerDialog((context)!!, object : OnDateSetListener { - override fun onDateSet(datepicker: DatePicker, selectedyear: Int, selectedmonth: Int, selectedday: Int) { - var selectedmonth = selectedmonth - mDateEditText!!.setText( - (selectedyear.toString() + "-" - + String.format("%02d", (selectedmonth + 1.also { selectedmonth = it })) + "-" - + String.format("%02d", selectedday)) - ) - setDate(selectedyear, selectedmonth, selectedday) - } - }, mYear, mMonth, mDay) - mDatePicker.setTitle("Select date") - mDatePicker.show() - } - }) - mTimeInEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - if ((mTimeInEditText!!.text.toString() == "")) { - val mcurrentTime = Calendar.getInstance() - mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesIn = mcurrentTime[Calendar.MINUTE] - } else { - mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - val mTimePicker: TimePickerDialog - mTimePicker = TimePickerDialog(context, object : OnTimeSetListener { - override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) { - val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute) - setTime(selectedMinute, selectedHour) - mTimeInEditText!!.setText(ddTime) - } - }, mHoursIn, mMinutesIn, true) //Yes 24 hour time - mTimePicker.setTitle("Select Time") - mTimePicker.show() - } - }) - mTimeOutEditText!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(v: View) { - if ((mTimeOutEditText!!.text.toString() == "")) { - val mcurrentTime = Calendar.getInstance() - mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesOut = mcurrentTime[Calendar.MINUTE] - } else { - mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - val mTimePicker: TimePickerDialog - mTimePicker = TimePickerDialog(context, object : OnTimeSetListener { - override fun onTimeSet(timePicker: TimePicker, selectedHour: Int, selectedMinute: Int) { - val ddTime = String.format("%02d", selectedHour) + ":" + String.format("%02d", selectedMinute) - setTime2(selectedMinute, selectedHour) - mTimeOutEditText!!.setText(ddTime) - } - }, mHoursOut, mMinutesOut, true) //Yes 24 hour time - mTimePicker.setTitle("Select Time") - mTimePicker.show() - } - }) - mTimeInEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mTimeOutEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mBreakEditText!!.addTextChangedListener(object : TextWatcher { - override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, aft: Int) {} - override fun afterTextChanged(s: Editable) { - setDuration() - calculateTotalPay() - } - }) - mUnitEditText!!.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun afterTextChanged(editable: Editable) { -// if(mRadioButtonTwo.isChecked()) { -// mUnits = 0.0f; -// if (!mUnitEditText.getText().toString().equals("")){ -// mUnits = Float.parseFloat(mUnitEditText.getText().toString()); -// } -// mPayRate = 0.0f; -// if (!mPayRateEditText.getText().toString().equals("")){ -// mPayRate = Float.parseFloat(mPayRateEditText.getText().toString()); -// } -// Float total = mPayRate * mUnits; -// mTotalPayTextView.setText(String.valueOf(total)); -// } - calculateTotalPay() - } - }) - mPayRateEditText!!.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} - override fun afterTextChanged(editable: Editable) { - calculateTotalPay() - } - }) - val SubmitProduct = rootView.findViewById(R.id.submit) as Button - SubmitProduct.setOnClickListener(object : View.OnClickListener { - override fun onClick(view: View) { - saveProduct() - } - }) - return rootView - } - - private fun calculateTotalPay() { - var total = 0.0f - mPayRate = 0.0f - if (mRadioButtonTwo!!.isChecked) { - mUnits = 0.0f - if (mUnitEditText!!.text.toString() != "") { - mUnits = mUnitEditText!!.text.toString().toFloat() - } - if (mPayRateEditText!!.text.toString() != "") { - mPayRate = mPayRateEditText!!.text.toString().toFloat() - } - total = mPayRate * mUnits - mTotalPayTextView!!.text = total.toString() - } else if (mRadioButtonOne!!.isChecked) { - if (mPayRateEditText!!.text.toString() != "") { - mPayRate = mPayRateEditText!!.text.toString().toFloat() - total = mPayRate * calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks) - } - } - mTotalPayTextView!!.text = String.format("%.2f", total) - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - menu.clear() - } - - private fun saveProduct() { - val typeString = mType - val descriptionString = mLocationEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(descriptionString)) { - Toast.makeText(context, "please insert Location", Toast.LENGTH_SHORT).show() - return - } - val dateString = mDateEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(dateString)) { - Toast.makeText(context, "please insert Date", Toast.LENGTH_SHORT).show() - return - } - var timeInString: String = "" - var timeOutString: String = "" - var breaks = 0 - var units = 0f - var duration = 0f - var payRate = 0f - val payRateString = mPayRateEditText!!.text.toString().trim { it <= ' ' } - if (!TextUtils.isEmpty(payRateString)) { - payRate = payRateString.toFloat() - } - var totalPay = 0f - if ((typeString == "Hourly")) { - timeInString = mTimeInEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(timeInString)) { - Toast.makeText(context, "please insert Time in", Toast.LENGTH_SHORT).show() - return - } - timeOutString = mTimeOutEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(timeOutString)) { - Toast.makeText(context, "please insert Time out", Toast.LENGTH_SHORT).show() - return - } - val breakMins = mBreakEditText!!.text.toString().trim { it <= ' ' } - if (!TextUtils.isEmpty(breakMins)) { - breaks = breakMins.toInt() - if ((breaks.toFloat() / 60) > calculateDurationWithoutBreak(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut)) { - Toast.makeText(context, "Break larger than duration", Toast.LENGTH_SHORT).show() - return - } - } - duration = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, breaks) - totalPay = duration * payRate - } else if ((typeString == "Piece Rate")) { - val unitsString = mUnitEditText!!.text.toString().trim { it <= ' ' } - if (TextUtils.isEmpty(unitsString) || unitsString.toFloat() <= 0) { - Toast.makeText(context, "Insert Units", Toast.LENGTH_SHORT).show() - return - } else { - units = unitsString.toFloat() - } - duration = 0f - totalPay = units * payRate - } - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeString) - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionString) - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateString) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInString) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutString) - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, duration) - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breaks) - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, units) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payRate) - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) - if (mCurrentProductUri == null) { - val newUri = activity.contentResolver.insert(ShiftsEntry.CONTENT_URI, values) - if (newUri == null) { - Toast.makeText(context, getString(R.string.insert_item_failed), - Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, getString(R.string.insert_item_successful), - Toast.LENGTH_SHORT).show() - } - } else { - val rowsAffected = activity.contentResolver.update(mCurrentProductUri!!, values, null, null) - if (rowsAffected == 0) { - Toast.makeText(context, getString(R.string.update_item_failed), - Toast.LENGTH_SHORT).show() - } else { - Toast.makeText(context, getString(R.string.update_item_successful), - Toast.LENGTH_SHORT).show() - } - } - (activity.fragmentManager)!!.popBackStack("main", 0) - } - - private fun setDuration() { - val mcurrentTime = Calendar.getInstance() - mBreaks = 0 - if (mBreakEditText!!.text.toString() != "") { - mBreaks = mBreakEditText!!.text.toString().toInt() - } - if ((mTimeOutEditText!!.text.toString() == "")) { - mHoursOut = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesOut = mcurrentTime[Calendar.MINUTE] - } else { - mHoursOut = (mTimeOutEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesOut = (mTimeOutEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - if ((mTimeInEditText!!.text.toString() == "")) { - mHoursIn = mcurrentTime[Calendar.HOUR_OF_DAY] - mMinutesIn = mcurrentTime[Calendar.MINUTE] - } else { - mHoursIn = (mTimeInEditText!!.text.toString().subSequence(0, 2)).toString().toInt() - mMinutesIn = (mTimeInEditText!!.text.toString().subSequence(3, 5)).toString().toInt() - } - mDurationTextView!!.text = calculateDuration(mHoursIn, mMinutesIn, mHoursOut, mMinutesOut, mBreaks).toString() + " hours" - } - - private fun setDate(year: Int, month: Int, day: Int) { - mYear = year - mMonth = month - mDay = day - } - - private fun setTime(minutes: Int, hours: Int) { - mMinutesIn = minutes - mHoursIn = hours - } - - private fun setTime2(minutes: Int, hours: Int) { - mMinutesOut = minutes - mHoursOut = hours - } - - private fun calculateDuration(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int, breaks: Int): Float { - val duration: Float - if (hoursOut > hoursIn) { - duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60) - } else { - duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - (breaks.toFloat() / 60) + 24) - } - val s = String.format("%.2f", duration) - return s.toFloat() - } - - private fun calculateDurationWithoutBreak(hoursIn: Int, minutesIn: Int, hoursOut: Int, minutesOut: Int): Float { - val duration: Float - if (hoursOut > hoursIn) { - duration = ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) - } else { - duration = (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24) - } - val s = String.format("%.2f", duration) - return s.toFloat() - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - progressBarAI!!.visibility = View.VISIBLE - scrollView!!.visibility = View.GONE - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader((context)!!, (mCurrentProductUri)!!, - projection, null, null, null) - } - - override fun onLoaderReset(loader: Loader) {} - - override fun onLoadFinished(loader: Loader, cursor: Cursor?) { - progressBarAI!!.visibility = View.GONE - scrollView!!.visibility = View.VISIBLE - if (cursor == null || cursor.count < 1) { - return - } - if (cursor.moveToFirst()) { - val descriptionColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - val dateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE) - val timeInColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - val timeOutColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - val breakColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK) - val durationColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION) - val typeColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE) - val unitColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT) - val payrateColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE) - val totalPayColumnIndex = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val type = cursor.getString(typeColumnIndex) - val description = cursor.getString(descriptionColumnIndex) - val date = cursor.getString(dateColumnIndex) - val timeIn = cursor.getString(timeInColumnIndex) - val timeOut = cursor.getString(timeOutColumnIndex) - val breaks = cursor.getInt(breakColumnIndex) - val duration = cursor.getFloat(durationColumnIndex) - val unit = cursor.getFloat(unitColumnIndex) - val payrate = cursor.getFloat(payrateColumnIndex) - val totalPay = cursor.getFloat(totalPayColumnIndex) - mLocationEditText!!.setText(description) - mDateEditText!!.setText(date) - if ((type == "Hourly") || (type == "hourly")) { - mRadioButtonOne!!.isChecked = true - mRadioButtonTwo!!.isChecked = false - mTimeInEditText!!.setText(timeIn) - mTimeOutEditText!!.setText(timeOut) - mBreakEditText!!.setText(Integer.toString(breaks)) - mDurationTextView!!.text = String.format("%.2f", duration) + " Hours" - } else if ((type == "Piece Rate")) { - mRadioButtonOne!!.isChecked = false - mRadioButtonTwo!!.isChecked = true - mUnitEditText!!.setText(java.lang.Float.toString(unit)) - } - mPayRateEditText!!.setText(String.format("%.2f", payrate)) - mTotalPayTextView!!.text = String.format("%.2f", totalPay) - } - } - - companion object { - private val EXISTING_PRODUCT_LOADER = 0 - var mRadioGroup: RadioGroup? = null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt deleted file mode 100644 index 2b09b09..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FragmentMain.kt +++ /dev/null @@ -1,459 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.Manifest -import android.annotation.SuppressLint -import android.app.Activity -import android.app.AlertDialog -import android.content.ContentValues -import android.content.DialogInterface -import android.content.Intent -import android.content.pm.PackageManager -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.os.Environment -import android.os.StrictMode -import android.os.StrictMode.VmPolicy -import android.util.Log -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import android.widget.ListView -import android.widget.Toast -import androidx.core.app.ActivityCompat -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.ajts.androidmads.library.SQLiteToExcel -import com.ajts.androidmads.library.SQLiteToExcel.ExportListener -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import com.appttude.h_mal.farmr.data.ShiftsDbHelper -import com.google.android.material.floatingactionbutton.FloatingActionButton -import java.io.File - -class FragmentMain : Fragment(), LoaderManager.LoaderCallbacks { - var mCursorAdapter: ShiftsCursorAdapter? = null - var shiftsDbhelper: ShiftsDbHelper? = null - lateinit var defaultLoaderCallback: LoaderManager.LoaderCallbacks - lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - val rootView = inflater.inflate(R.layout.fragment_main, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - - activity.setActionBarTitle(getString(R.string.app_name)) - activity.filter = activity.getSharedPreferences("PREFS", 0) - activity.sortOrder = activity.filter?.getString("Filter", null) - defaultLoaderCallback = this - val productListView = rootView.findViewById(R.id.list_item_view) as ListView - val emptyView = rootView.findViewById(R.id.empty_view) - productListView.emptyView = emptyView - mCursorAdapter = ShiftsCursorAdapter(activity, null) - productListView.adapter = mCursorAdapter - loaderManager.initLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - val fab = rootView.findViewById(R.id.fab1) - fab.setOnClickListener { - val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction() - fragmentTransaction.replace(R.id.container, FragmentAddItem()).addToBackStack("additem").commit() - } - return rootView - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.delete_all -> { - deleteAllProducts() - return true - } - - R.id.help -> { - AlertDialog.Builder(context) - .setTitle("Help & Support:") - .setView(R.layout.dialog_layout) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show() - return true - } - - R.id.filter_data -> { - val fragmentTransaction: FragmentTransaction = activity.fragmentManager!!.beginTransaction() - fragmentTransaction.replace(R.id.container, FilterDataFragment()).addToBackStack("filterdata").commit() - return true - } - - R.id.sort_data -> { - sortData() - return true - } - - R.id.clear_filter -> { - activity.args = null - activity.selection = null - NEW_LOADER = 0 - loaderManager.restartLoader(DEFAULT_LOADER, null, this) - return true - } - - R.id.export_data -> { - if (checkStoragePermissions(activity)) { - AlertDialog.Builder(context) - .setTitle("Export?") - .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() - } else { - Toast.makeText(context, "Storage permissions required", Toast.LENGTH_SHORT).show() - } - return true - } - - R.id.action_favorite -> { - AlertDialog.Builder(context) - .setTitle("Info:") - .setMessage(retrieveInfo()) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> }.create().show() - return true - } - } - return super.onOptionsItemSelected(item) - } - - private fun sortData() { - val grpname = arrayOf("Added", "Date", "Name") - val sortQuery = arrayOf("") - var checkedItem = -1 - if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry._ID)) { - checkedItem = 0 - sortQuery[0] = ShiftsEntry._ID - } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DATE)) { - checkedItem = 1 - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE - } else if (activity.sortOrder != null && activity.sortOrder!!.contains(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) { - checkedItem = 2 - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION - } - val alt_bld = AlertDialog.Builder(context) - //alt_bld.setIcon(R.drawable.icon); - alt_bld.setTitle("Sort by:") - alt_bld.setSingleChoiceItems(grpname, checkedItem, DialogInterface.OnClickListener { dialog, item -> - when (item) { - 0 -> { - sortQuery[0] = ShiftsEntry._ID - return@OnClickListener - } - - 1 -> { - sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DATE - return@OnClickListener - } - - 2 -> sortQuery[0] = ShiftsEntry.COLUMN_SHIFT_DESCRIPTION - } - }).setPositiveButton("Ascending") { dialog, id -> - activity.sortOrder = sortQuery[0] + " ASC" - activity.filter!!.edit().putString("Filter", activity.sortOrder).apply() - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - dialog.dismiss() - }.setNegativeButton("Descending") { dialog, id -> - activity.sortOrder = sortQuery[0] + " DESC" - activity.filter!!.edit().putString("Filter", activity.sortOrder).apply() - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - dialog.dismiss() - } - val alert = alt_bld.create() - alert.show() - } - - private fun deleteAllProducts() { - AlertDialog.Builder(context) - .setTitle("Delete?") - .setMessage("Are you sure you want to delete all date?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> - val rowsDeleted = activity.contentResolver.delete(ShiftsEntry.CONTENT_URI, null, null) - Toast.makeText(context, "$rowsDeleted Items Deleted", Toast.LENGTH_SHORT).show() - }.create().show() - } - - override fun onResume() { - super.onResume() - if (NEW_LOADER > DEFAULT_LOADER) { - loaderManager.restartLoader(DEFAULT_LOADER, null, defaultLoaderCallback) - println("reloading loader") - } - } - - private fun ExportData() { - val permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() - return - } - shiftsDbhelper = ShiftsDbHelper(context) - val database = shiftsDbhelper!!.writableDatabase - val projection_export = arrayOf( - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val cursor = activity.contentResolver.query( - ShiftsEntry.CONTENT_URI, - projection_export, - activity.selection, - activity.args, - activity.sortOrder) - database.delete(ShiftsEntry.TABLE_NAME_EXPORT, null, null) - var totalDuration = 0.00f - var totalUnits = 0.00f - var totalPay = 0.00f - try { - while (cursor!!.moveToNext()) { - val descriptionColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) - val dateColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) - val timeInColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) - val timeOutColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) - val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val breakOutColumnIndex = cursor.getInt(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_BREAK)) - val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val payrateColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_PAYRATE)) - val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - totalUnits = totalUnits + unitColumnIndex - totalDuration = totalDuration + durationColumnIndex - totalPay = totalPay + totalpayColumnIndex - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, descriptionColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, dateColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, timeInColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, timeOutColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, breakOutColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, durationColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, typeColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, unitColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, payrateColumnIndex) - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalpayColumnIndex) - database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) - } - } catch (e: Exception) { - Log.e("FragmentMain", "ExportData: ", e) - } finally { - val values = ContentValues() - values.put(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, "") - values.put(ShiftsEntry.COLUMN_SHIFT_DATE, "") - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_IN, "") - values.put(ShiftsEntry.COLUMN_SHIFT_TIME_OUT, "") - values.put(ShiftsEntry.COLUMN_SHIFT_BREAK, "Total duration:") - values.put(ShiftsEntry.COLUMN_SHIFT_DURATION, totalDuration) - values.put(ShiftsEntry.COLUMN_SHIFT_TYPE, "Total units:") - values.put(ShiftsEntry.COLUMN_SHIFT_UNIT, totalUnits) - values.put(ShiftsEntry.COLUMN_SHIFT_PAYRATE, "Total pay:") - values.put(ShiftsEntry.COLUMN_SHIFT_TOTALPAY, totalPay) - database.insert(ShiftsEntry.TABLE_NAME_EXPORT, null, values) - cursor!!.close() - } - val savePath = Environment.getExternalStorageDirectory().toString() + "/ShifttrackerTemp" - val file = File(savePath) - if (!file.exists()) { - file.mkdirs() - } - val sqLiteToExcel = SQLiteToExcel(context, "shifts.db", savePath) - sqLiteToExcel.exportSingleTable("shiftsexport", "shifthistory.xls", object : ExportListener { - override fun onStart() {} - override fun onCompleted(filePath: String) { - Toast.makeText(context, filePath, Toast.LENGTH_SHORT).show() - val newPath = Uri.parse("file://$savePath/shifthistory.xls") - val builder = VmPolicy.Builder() - StrictMode.setVmPolicy(builder.build()) - val emailintent = Intent(Intent.ACTION_SEND) - emailintent.type = "application/vnd.ms-excel" - emailintent.putExtra(Intent.EXTRA_SUBJECT, "historic shifts") - emailintent.putExtra(Intent.EXTRA_TEXT, "I'm email body.") - emailintent.putExtra(Intent.EXTRA_STREAM, newPath) - startActivity(Intent.createChooser(emailintent, "Send Email")) - } - - override fun onError(e: Exception) { - println("Error msg: $e") - Toast.makeText(context, "Failed to Export data", Toast.LENGTH_SHORT).show() - } - }) - } - - private fun retrieveInfo(): String { - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val cursor = activity.contentResolver.query( - ShiftsEntry.CONTENT_URI, - projection, - activity.selection, - activity.args, - activity.sortOrder) - var totalDuration = 0.0f - var countOfTypeH = 0 - var countOfTypeP = 0 - var totalUnits = 0f - var totalPay = 0f - var lines = 0 - try { - while (cursor!!.moveToNext()) { - val durationColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val typeColumnIndex = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val unitColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val totalpayColumnIndex = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - totalDuration = totalDuration + durationColumnIndex - if (typeColumnIndex.contains("Hourly")) { - countOfTypeH = countOfTypeH + 1 - } else if (typeColumnIndex.contains("Piece")) { - countOfTypeP = countOfTypeP + 1 - } - totalUnits = totalUnits + unitColumnIndex - totalPay = totalPay + totalpayColumnIndex - } - } finally { - if (cursor != null && cursor.count > 0) { - lines = cursor.count - cursor.close() - } - } - return buildInfoString(totalDuration, countOfTypeH, countOfTypeP, totalUnits, totalPay, lines) - } - - @SuppressLint("DefaultLocale") - fun buildInfoString(totalDuration: Float, countOfTypeH: Int, countOfTypeP: Int, totalUnits: Float, totalPay: Float, lines: Int): String { - var textString: String - textString = "$lines Shifts" - if (countOfTypeH != 0 && countOfTypeP != 0) { - textString = "$textString ($countOfTypeH Hourly/$countOfTypeP Piece Rate)" - } - if (countOfTypeH != 0) { - textString = """ - $textString - Total Hours: ${String.format("%.2f", totalDuration)} - """.trimIndent() - } - if (countOfTypeP != 0) { - textString = """ - $textString - Total Units: ${String.format("%.2f", totalUnits)} - """.trimIndent() - } - if (totalPay != 0f) { - textString = """ - $textString - Total Pay: ${"$"}${String.format("%.2f", totalPay)} - """.trimIndent() - } - return textString - } - - override fun onCreateLoader(i: Int, bundle: Bundle?): Loader { - val projection = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader(context!!, - ShiftsEntry.CONTENT_URI, - projection, - activity.selection, - activity.args, - activity.sortOrder) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor) { - mCursorAdapter!!.swapCursor(cursor) - } - - override fun onLoaderReset(loader: Loader) { - mCursorAdapter!!.swapCursor(null) - } - - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - println("request code$requestCode") - if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { - if (grantResults.size > 0 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - exportDialog() - } else { - Toast.makeText(context, "Storage Permissions denied", Toast.LENGTH_SHORT).show() - } - } - } - - fun exportDialog() { - AlertDialog.Builder(context) - .setTitle("Export?") - .setMessage("Exporting current filtered data. Continue?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> ExportData() }.create().show() - } - - companion object { - const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 - private const val DEFAULT_LOADER = 0 - var NEW_LOADER = 0 - - // // Storage Permissions - // private static final int REQUEST_EXTERNAL_STORAGE = 1; - // private static String[] PERMISSIONS_STORAGE = { - // Manifest.permission.READ_EXTERNAL_STORAGE, - // Manifest.permission.WRITE_EXTERNAL_STORAGE - // }; - // /** - // * Checks if the app has permission to write to device storage - // * - // * If the app does not has permission then the user will be prompted to grant permissions - // * - // * @param activity - // */ - // public static void verifyStoragePermissions(Activity activity) { - // // Check if we have write permission - // int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); - // - // if (permission != PackageManager.PERMISSION_GRANTED) { - // // We don't have permission so prompt the user - // ActivityCompat.requestPermissions( - // activity, - // PERMISSIONS_STORAGE, - // REQUEST_EXTERNAL_STORAGE - // ); - // } - // } - fun checkStoragePermissions(activity: Activity?): Boolean { - var status = false - val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission == PackageManager.PERMISSION_GRANTED) { - status = true - } - return status - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt deleted file mode 100644 index eedc41d..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/FurtherInfoFragment.kt +++ /dev/null @@ -1,161 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.TextView -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentTransaction -import androidx.loader.app.LoaderManager -import androidx.loader.content.CursorLoader -import androidx.loader.content.Loader -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry - -class FurtherInfoFragment : Fragment(), LoaderManager.LoaderCallbacks { - private var typeTV: TextView? = null - private var descriptionTV: TextView? = null - private var dateTV: TextView? = null - private var times: TextView? = null - private var breakTV: TextView? = null - private var durationTV: TextView? = null - private var unitsTV: TextView? = null - private var payRateTV: TextView? = null - private var totalPayTV: TextView? = null - private var hourlyDetailHolder: LinearLayout? = null - private var unitsHolder: LinearLayout? = null - private var wholeView: LinearLayout? = null - private var progressBarFI: ProgressBar? = null - private var editButton: Button? = null - private var CurrentUri: Uri? = null - - lateinit var activity: MainActivity - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View { - // Inflate the layout for this fragment - val rootView: View = inflater.inflate(R.layout.fragment_futher_info, container, false) - setHasOptionsMenu(true) - activity = (requireActivity() as MainActivity) - activity.setActionBarTitle(getString(R.string.further_info_title)) - - progressBarFI = rootView.findViewById(R.id.progressBar_info) as ProgressBar? - wholeView = rootView.findViewById(R.id.further_info_view) as LinearLayout? - typeTV = rootView.findViewById(R.id.details_shift) as TextView? - descriptionTV = rootView.findViewById(R.id.details_desc) as TextView? - dateTV = rootView.findViewById(R.id.details_date) as TextView? - times = rootView.findViewById(R.id.details_time) as TextView? - breakTV = rootView.findViewById(R.id.details_breaks) as TextView? - durationTV = rootView.findViewById(R.id.details_duration) as TextView? - unitsTV = rootView.findViewById(R.id.details_units) as TextView? - payRateTV = rootView.findViewById(R.id.details_pay_rate) as TextView? - totalPayTV = rootView.findViewById(R.id.details_totalpay) as TextView? - editButton = rootView.findViewById(R.id.details_edit) as Button? - hourlyDetailHolder = rootView.findViewById(R.id.details_hourly_details) as LinearLayout? - unitsHolder = rootView.findViewById(R.id.details_units_holder) as LinearLayout? - val b: Bundle? = arguments - CurrentUri = Uri.parse(b!!.getString("uri")) - loaderManager.initLoader(DEFAULT_LOADER, null, this) - editButton!!.setOnClickListener(object : View.OnClickListener { - override fun onClick(view: View) { - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment: Fragment = FragmentAddItem() - fragment.arguments = b - fragmentTransaction.replace(R.id.container, fragment).addToBackStack("additem").commit() - } - }) - return rootView - } - - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - super.onCreateOptionsMenu(menu, inflater) - menu.clear() - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - progressBarFI!!.visibility = View.VISIBLE - wholeView!!.visibility = View.GONE - val projection: Array = arrayOf( - ShiftsEntry._ID, - ShiftsEntry.COLUMN_SHIFT_DESCRIPTION, - ShiftsEntry.COLUMN_SHIFT_DATE, - ShiftsEntry.COLUMN_SHIFT_TIME_IN, - ShiftsEntry.COLUMN_SHIFT_TIME_OUT, - ShiftsEntry.COLUMN_SHIFT_BREAK, - ShiftsEntry.COLUMN_SHIFT_DURATION, - ShiftsEntry.COLUMN_SHIFT_TYPE, - ShiftsEntry.COLUMN_SHIFT_PAYRATE, - ShiftsEntry.COLUMN_SHIFT_UNIT, - ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - return CursorLoader((context)!!, (CurrentUri)!!, - projection, null, null, null) - } - - override fun onLoadFinished(loader: Loader, cursor: Cursor) { - progressBarFI!!.visibility = View.GONE - wholeView!!.visibility = View.VISIBLE - if (cursor == null || cursor.count < 1) { - return - } - if (cursor.moveToFirst()) { - val descriptionColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - val dateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DATE) - val timeInColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - val timeOutColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - val breakColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_BREAK) - val durationColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_DURATION) - val typeColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TYPE) - val unitColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_UNIT) - val payrateColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_PAYRATE) - val totalPayColumnIndex: Int = cursor.getColumnIndex(ShiftsEntry.COLUMN_SHIFT_TOTALPAY) - val type: String = cursor.getString(typeColumnIndex) - val description: String = cursor.getString(descriptionColumnIndex) - val date: String = cursor.getString(dateColumnIndex) - val timeIn: String = cursor.getString(timeInColumnIndex) - val timeOut: String = cursor.getString(timeOutColumnIndex) - val breaks: Int = cursor.getInt(breakColumnIndex) - val duration: Float = cursor.getFloat(durationColumnIndex) - val unit: Float = cursor.getFloat(unitColumnIndex) - val payrate: Float = cursor.getFloat(payrateColumnIndex) - val totalPay: Float = cursor.getFloat(totalPayColumnIndex) - var durationString: String = ShiftsCursorAdapter.Companion.timeValues(duration).get(0) + " Hours " + ShiftsCursorAdapter.Companion.timeValues(duration).get(1) + " Minutes " - if (breaks != 0) { - durationString = durationString + " (+ " + Integer.toString(breaks) + " minutes break)" - } - typeTV!!.text = type - descriptionTV!!.text = description - dateTV!!.text = date - var totalPaid: String? = "" - val currency: String = "$" - if ((type == "Hourly")) { - hourlyDetailHolder!!.visibility = View.VISIBLE - unitsHolder!!.visibility = View.GONE - times!!.text = timeIn + " - " + timeOut - breakTV!!.text = Integer.toString(breaks) + "mins" - durationTV!!.text = durationString - totalPaid = (String.format("%.2f", duration) + " Hours @ " + currency + String.format("%.2f", payrate) + " per Hour" + "\n" - + "Equals: " + currency + String.format("%.2f", totalPay)) - } else if ((type == "Piece Rate")) { - hourlyDetailHolder!!.visibility = View.GONE - unitsHolder!!.visibility = View.VISIBLE - unitsTV!!.text = String.format("%.2f", unit) - totalPaid = (String.format("%.2f", unit) + " Units @ " + currency + String.format("%.2f", payrate) + " per Unit" + "\n" - + "Equals: " + currency + String.format("%.2f", totalPay)) - } - payRateTV!!.text = String.format("%.2f", payrate) - totalPayTV!!.text = totalPaid - } - } - - override fun onLoaderReset(loader: Loader) {} - - companion object { - private val DEFAULT_LOADER: Int = 0 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt deleted file mode 100644 index a0cbd9c..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/MainActivity.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.Manifest -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.Menu -import android.view.View -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar -import androidx.core.app.ActivityCompat -import androidx.fragment.app.FragmentManager - -class MainActivity : AppCompatActivity() { - var filter: SharedPreferences? = null - var context: Context? = null - var sortOrder: String? = null - var selection: String? = null - var args: Array? = null - private var toolbar: Toolbar? = null - var fragmentManager: FragmentManager? = null - private var currentFragment: String? = null - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.main_view) - verifyStoragePermissions(this) - toolbar = findViewById(R.id.toolbar) as Toolbar - setSupportActionBar(toolbar) - fragmentManager = supportFragmentManager - val fragmentTransaction = fragmentManager?.beginTransaction() - fragmentTransaction?.replace(R.id.container, FragmentMain())?.addToBackStack("main")?.commit() - fragmentManager?.addOnBackStackChangedListener { - val f = fragmentManager?.fragments - val frag = f?.get(0) - currentFragment = frag?.javaClass?.simpleName - } - } - - override fun onCreateOptionsMenu(menu: Menu): Boolean { - // Inflate the menu; this adds items to the action bar if it is present. - menuInflater.inflate(R.menu.menu_main, menu) - return true - } - - override fun onBackPressed() { - when (currentFragment) { - "FragmentMain" -> { - AlertDialog.Builder(this) - .setTitle("Leave?") - .setMessage("Are you sure you want to exit Farmr?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> - val intent = Intent(Intent.ACTION_MAIN) - intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP - intent.addCategory(Intent.CATEGORY_HOME) - startActivity(intent) - finish() - System.exit(0) - }.create().show() - return - } - - "FragmentAddItem" -> { - if (FragmentAddItem.Companion.mRadioGroup!!.checkedRadioButtonId == -1) { - fragmentManager!!.popBackStack() - } else { - AlertDialog.Builder(this) - .setTitle("Discard Changes?") - .setMessage("Are you sure you want to discard changes?") - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes) { arg0, arg1 -> fragmentManager!!.popBackStack() }.create().show() - } - return - } - - else -> if (fragmentManager!!.backStackEntryCount > 1) { - fragmentManager!!.popBackStack() - } - } - } - - fun setActionBarTitle(title: String?) { - toolbar!!.title = title - } - - // Storage Permissions - private val REQUEST_EXTERNAL_STORAGE = 1 - private val PERMISSIONS_STORAGE = arrayOf( - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.WRITE_EXTERNAL_STORAGE - ) - - /** - * Checks if the app has permission to write to device storage - * - * If the app does not has permission then the user will be prompted to grant permissions - * - * @param activity - */ - fun verifyStoragePermissions(activity: Activity?) { - // Check if we have write permission - val permission = ActivityCompat.checkSelfPermission(activity!!, Manifest.permission.WRITE_EXTERNAL_STORAGE) - if (permission != PackageManager.PERMISSION_GRANTED) { - // We don't have permission so prompt the user - ActivityCompat.requestPermissions( - activity, - PERMISSIONS_STORAGE, - REQUEST_EXTERNAL_STORAGE - ) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt deleted file mode 100644 index 0163f95..0000000 --- a/app/src/main/java/com/appttude/h_mal/farmr/ShiftsCursorAdapter.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.appttude.h_mal.farmr - -import android.app.AlertDialog -import android.content.ContentUris -import android.content.Context -import android.content.DialogInterface -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.View.OnLongClickListener -import android.view.ViewGroup -import android.widget.CursorAdapter -import android.widget.ImageView -import android.widget.TextView -import androidx.fragment.app.FragmentTransaction -import com.appttude.h_mal.farmr.data.ShiftProvider -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry -import kotlin.math.floor - -/** - * Created by h_mal on 26/12/2017. - */ -class ShiftsCursorAdapter constructor(private val activity: MainActivity, c: Cursor?) : CursorAdapter(activity, c, 0) { - private var mContext: Context? = null - var shiftProvider: ShiftProvider? = null - override fun newView(context: Context, cursor: Cursor, parent: ViewGroup): View { - return LayoutInflater.from(context).inflate(R.layout.list_item_1, parent, false) - } - - override fun bindView(view: View, context: Context, cursor: Cursor) { - mContext = context - val descriptionTextView: TextView = view.findViewById(R.id.location) as TextView - val dateTextView: TextView = view.findViewById(R.id.date) as TextView - val totalPay: TextView = view.findViewById(R.id.total_pay) as TextView - val hoursView: TextView = view.findViewById(R.id.hours) as TextView - val h: TextView = view.findViewById(R.id.h) as TextView - val minutesView: TextView = view.findViewById(R.id.minutes) as TextView - val m: TextView = view.findViewById(R.id.m) as TextView - val editView: ImageView = view.findViewById(R.id.imageView) as ImageView - h.text = "h" - m.text = "m" - val typeColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TYPE)) - val descriptionColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) - val dateColumnIndex: String = cursor.getString(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DATE)) - val durationColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_DURATION)) - val unitsColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_UNIT)) - val totalpayColumnIndex: Float = cursor.getFloat(cursor.getColumnIndexOrThrow(ShiftsEntry.COLUMN_SHIFT_TOTALPAY)) - descriptionTextView.text = descriptionColumnIndex - dateTextView.text = newDate(dateColumnIndex) - totalPay.text = String.format("%.2f", totalpayColumnIndex) - if ((typeColumnIndex == "Piece Rate") && durationColumnIndex == 0f) { - hoursView.text = unitsColumnIndex.toString() - h.text = "" - minutesView.text = "" - m.text = "pcs" - } else // if(typeColumnIndex.equals("Hourly") || typeColumnIndex.equals("hourly")) - { - hoursView.text = timeValues(durationColumnIndex).get(0) - minutesView.text = timeValues(durationColumnIndex).get(1) - } - val ID: Long = cursor.getLong(cursor.getColumnIndexOrThrow(ShiftsEntry._ID)) - val currentProductUri: Uri = ContentUris.withAppendedId(ShiftsEntry.CONTENT_URI, ID) - val b: Bundle = Bundle() - b.putString("uri", currentProductUri.toString()) - view.setOnClickListener { // activity.clickOnViewItem(ID); - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment2: FurtherInfoFragment = FurtherInfoFragment() - fragment2.arguments = b - fragmentTransaction.replace(R.id.container, fragment2).addToBackStack("furtherinfo").commit() - } - editView.setOnClickListener { - val fragmentTransaction: FragmentTransaction = (activity.fragmentManager)!!.beginTransaction() - val fragment3: FragmentAddItem = FragmentAddItem() - fragment3.arguments = b - fragmentTransaction.replace(R.id.container, fragment3).addToBackStack("additem").commit() - } - view.setOnLongClickListener { - println("long click: $ID") - val builder: AlertDialog.Builder = AlertDialog.Builder(mContext) - builder.setMessage("Are you sure you want to delete") - builder.setPositiveButton("delete") { dialog, id -> deleteProduct(ID) } - builder.setNegativeButton("cancel") { dialog, id -> - dialog?.dismiss() - } - val alertDialog: AlertDialog = builder.create() - alertDialog.show() - true - } - } - - private fun deleteProduct(id: Long) { - val args: Array = arrayOf(id.toString()) - // String whereClause = String.format(ShiftsEntry._ID + " in (%s)", new Object[] { TextUtils.join(",", Collections.nCopies(args.length, "?")) }); //for deleting multiple lines - mContext!!.contentResolver.delete(ShiftsEntry.CONTENT_URI, ShiftsEntry._ID + "=?", args) - } - - private fun newDate(dateString: String): String { - var returnString: String? = "01/01/2010" - val year: String = dateString.substring(0, 4) - val month: String = dateString.substring(5, 7) - val day: String = dateString.substring(8) - returnString = "$day-$month-$year" - return returnString - } - - companion object { - fun timeValues(duration: Float): Array { - val hours: Int = floor(duration.toDouble()).toInt() - val minutes: Int = ((duration - hours) * 60).toInt() - val hoursString: String = hours.toString() + "" - val minutesString: String = String.format("%02d", minutes) - return arrayOf(hoursString, minutesString) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt new file mode 100644 index 0000000..3ade301 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BackPressedListener.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.base + +interface BackPressedListener { + fun onBackPressed(): Boolean +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt new file mode 100644 index 0000000..6ddc382 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseActivity.kt @@ -0,0 +1,42 @@ +package com.appttude.h_mal.farmr.base + +import android.content.Intent +import androidx.appcompat.app.AppCompatActivity +import com.appttude.h_mal.farmr.utils.displayToast + +abstract class BaseActivity : AppCompatActivity() { + + + /** + * Creates a loading view which to be shown during async operations + * + * #setOnClickListener(null) is an ugly work around to prevent under being clicked during + * loading + */ + + fun startActivity(activity: Class) { + val intent = Intent(this, activity) + startActivity(intent) + } + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onStarted() {} + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onSuccess(data: Any?) {} + + /** + * Called in case of failure or some error emitted from the liveData in viewModel + */ + open fun onFailure(error: Any?) { + if (error is String) displayToast(error) + } + + fun setTitleInActionBar(title: String) { + supportActionBar?.title = title + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt new file mode 100644 index 0000000..055e5f3 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseApplication.kt @@ -0,0 +1,31 @@ +package com.appttude.h_mal.farmr.base + +import android.app.Application +import com.appttude.h_mal.farmr.data.RepositoryImpl +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import org.kodein.di.Kodein +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.androidXModule +import org.kodein.di.generic.bind +import org.kodein.di.generic.instance +import org.kodein.di.generic.provider +import org.kodein.di.generic.singleton + +abstract class BaseApplication() : Application(), KodeinAware { + + // Kodein creation of modules to be retrieve within the app + override val kodein = Kodein.lazy { + import(androidXModule(this@BaseApplication)) + + bind() from singleton { createDatabase() } + bind() from singleton { createPrefs() } + bind() from singleton { RepositoryImpl(instance(), instance()) } + + bind() from provider { ApplicationViewModelFactory(instance()) } + } + + abstract fun createDatabase(): LegacyDatabase + abstract fun createPrefs(): PreferenceProvider +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt new file mode 100644 index 0000000..a74edba --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseFragment.kt @@ -0,0 +1,81 @@ +package com.appttude.h_mal.farmr.base + +import android.os.Bundle +import android.view.View +import androidx.annotation.LayoutRes +import androidx.fragment.app.Fragment +import androidx.lifecycle.ViewModelLazy +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getGenericClassAt +import com.appttude.h_mal.farmr.utils.popBackStack +import com.appttude.h_mal.farmr.viewmodel.ApplicationViewModelFactory +import org.kodein.di.KodeinAware +import org.kodein.di.android.x.kodein +import org.kodein.di.generic.instance +import kotlin.properties.Delegates + +@Suppress("EmptyMethod", "EmptyMethod") +abstract class BaseFragment(@LayoutRes contentLayoutId: Int) : + Fragment(contentLayoutId), KodeinAware { + + override val kodein by kodein() + private val factory by instance() + + val viewModel: V by getViewModel() + + private fun getViewModel(): Lazy = + ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore }, + factoryProducer = { factory } ) + + var mActivity: BaseActivity? = null + + private var shortAnimationDuration by Delegates.notNull() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + shortAnimationDuration = resources.getInteger(android.R.integer.config_shortAnimTime) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + mActivity = requireActivity() as BaseActivity + configureObserver() + } + + private fun configureObserver() { + viewModel.uiState.observe(viewLifecycleOwner) { + when (it) { + is ViewState.HasStarted -> onStarted() + is ViewState.HasData<*> -> onSuccess(it.data) + is ViewState.HasError<*> -> onFailure(it.error) + } + } + } + + /** + * Called in case of starting operation liveData in viewModel + */ + open fun onStarted() { + mActivity?.onStarted() + } + + /** + * Called in case of success or some data emitted from the liveData in viewModel + */ + open fun onSuccess(data: Any?) { + mActivity?.onSuccess(data) + } + + /** + * Called in case of failure or some error emitted from the liveData in viewModel + */ + open fun onFailure(error: Any?) { + mActivity?.onFailure(error) + } + + fun setTitle(title: String) { + (requireActivity() as BaseActivity).setTitleInActionBar(title) + } + + fun popBackStack() = mActivity?.popBackStack() +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt new file mode 100644 index 0000000..fd34388 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseRecyclerAdapter.kt @@ -0,0 +1,42 @@ +package com.appttude.h_mal.farmr.base + +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.appttude.h_mal.farmr.utils.generateView + +open class BaseRecyclerAdapter( + @LayoutRes private val emptyViewId: Int, + @LayoutRes private val currentViewId: Int +): RecyclerView.Adapter() { + var list: List? = null + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return if (list.isNullOrEmpty()) { + val emptyViewHolder = parent.generateView(emptyViewId) + EmptyViewHolder(emptyViewHolder) + } else { + val currentViewHolder = parent.generateView(currentViewId) + CurrentViewHolder(currentViewHolder) + } + } + + override fun getItemCount(): Int { + return if (list.isNullOrEmpty()) 1 else list!!.size + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + when (holder) { + is EmptyViewHolder -> bindEmptyView(holder.itemView) + is CurrentViewHolder -> bindCurrentView(holder.itemView, position, list!![position]) + } + } + + open fun bindEmptyView(view: View) {} + open fun bindCurrentView(view: View, position: Int, data: T) {} + + class EmptyViewHolder(itemView: View): ViewHolder(itemView) + class CurrentViewHolder(itemView: View): ViewHolder(itemView) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt new file mode 100644 index 0000000..8ff7475 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/base/BaseViewModel.kt @@ -0,0 +1,25 @@ +package com.appttude.h_mal.farmr.base + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import com.appttude.h_mal.farmr.model.ViewState + +open class BaseViewModel: ViewModel() { + + private val _uiState = MutableLiveData() + val uiState: LiveData = _uiState + + + fun onStart() { + _uiState.postValue(ViewState.HasStarted) + } + + fun onSuccess(result: T) { + _uiState.postValue(ViewState.HasData(result)) + } + + protected fun onError(error: E) { + _uiState.postValue(ViewState.HasError(error)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt new file mode 100644 index 0000000..db97694 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/Repository.kt @@ -0,0 +1,24 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.Sortable + +interface Repository { + fun insertShiftIntoDatabase(shift: Shift): Boolean + fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean + fun readShiftsFromDatabase(): List? + fun readSingleShiftFromDatabase(id: Long): ShiftObject? + fun deleteSingleShiftFromDatabase(id: Long): Boolean + fun deleteAllShiftsFromDatabase(): Boolean + fun retrieveSortAndOrderFromPref(): Pair + fun setSortAndOrderFromPref(sortable: Sortable, order: Order) + fun retrieveFilteringDetailsInPrefs(): Map + fun setFilteringDetailsInPrefs( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt new file mode 100644 index 0000000..3af7652 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/RepositoryImpl.kt @@ -0,0 +1,71 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.Sortable + +class RepositoryImpl( + private val legacyDatabase: LegacyDatabase, + private val preferenceProvider: PreferenceProvider +): Repository { + override fun insertShiftIntoDatabase(shift: Shift): Boolean { + return legacyDatabase.insertShiftDataIntoDatabase(shift) != null + } + + override fun updateShiftIntoDatabase(id: Long, shift: Shift): Boolean { + return legacyDatabase.updateShiftDataIntoDatabase( + id = id, + typeString = shift.type.type, + descriptionString = shift.description, + dateString = shift.date, + timeInString = shift.timeIn ?: "", + timeOutString = shift.timeOut ?: "", + duration = shift.duration ?: 0f, + breaks = shift.breakMins ?: 0, + units = shift.units ?: 0f, + payRate = shift.rateOfPay, + totalPay = shift.totalPay + ) == 1 + } + + override fun readShiftsFromDatabase(): List? { + return legacyDatabase.readShiftsFromDatabase() + } + + override fun readSingleShiftFromDatabase(id: Long): ShiftObject? { + return legacyDatabase.readSingleShiftWithId(id) + } + + override fun deleteSingleShiftFromDatabase(id: Long): Boolean { + return legacyDatabase.deleteSingleShift(id) == 1 + } + + override fun deleteAllShiftsFromDatabase(): Boolean { + return legacyDatabase.deleteAllShiftsInDatabase() > 0 + } + + override fun retrieveSortAndOrderFromPref(): Pair { + return preferenceProvider.getSortableAndOrder() + } + + override fun setSortAndOrderFromPref(sortable: Sortable, order: Order) { + preferenceProvider.saveSortableAndOrder(sortable, order) + } + + override fun retrieveFilteringDetailsInPrefs(): Map { + return preferenceProvider.getFilteringDetails() + } + + override fun setFilteringDetailsInPrefs( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) { + preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt new file mode 100644 index 0000000..729ea9a --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/LegacyDatabase.kt @@ -0,0 +1,166 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import android.content.ContentResolver +import android.content.ContentUris +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.CONTENT_URI +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.model.Shift + +class LegacyDatabase(private val resolver: ContentResolver) { + + private val projection = arrayOf( + _ID, + COLUMN_SHIFT_DESCRIPTION, + COLUMN_SHIFT_DATE, + COLUMN_SHIFT_TIME_IN, + COLUMN_SHIFT_TIME_OUT, + COLUMN_SHIFT_BREAK, + COLUMN_SHIFT_DURATION, + COLUMN_SHIFT_TYPE, + COLUMN_SHIFT_UNIT, + COLUMN_SHIFT_PAYRATE, + COLUMN_SHIFT_TOTALPAY + ) + + // Create + fun insertShiftDataIntoDatabase( + shift: Shift + ): Uri? { + val values = ContentValues().apply { + put(COLUMN_SHIFT_TYPE, shift.type.type) + put(COLUMN_SHIFT_DESCRIPTION, shift.description) + put(COLUMN_SHIFT_DATE, shift.date) + put(COLUMN_SHIFT_TIME_IN, shift.timeIn ?: "00:00") + put(COLUMN_SHIFT_TIME_OUT, shift.timeOut ?: "00:00") + put(COLUMN_SHIFT_DURATION, shift.duration ?: 0.00f) + put(COLUMN_SHIFT_BREAK, shift.breakMins ?: 0) + put(COLUMN_SHIFT_UNIT, shift.units ?: 0.00f) + put(COLUMN_SHIFT_PAYRATE, shift.rateOfPay) + put(COLUMN_SHIFT_TOTALPAY, shift.totalPay) + } + return resolver.insert(CONTENT_URI, values) + } + + // Read + fun readShiftsFromDatabase(): List? { + val cursor = resolver.query( + CONTENT_URI, + projection, + null, null, null + ) ?: return null + val shifts = generateSequence { if (cursor.moveToNext()) cursor else null } + .map { it.getShift() } + .toList() + // close cursor after query operations + cursor.close() + + return shifts + } + + fun readSingleShiftWithId(id: Long): ShiftObject? { + val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id) + + val cursor = resolver.query( + itemUri, + projection, + null, null, null + ) ?: return null + cursor.moveToFirst() + + val shift = cursor.takeIf { it.moveToFirst() }?.run { getShift() } ?: return null + cursor.close() + return shift + } + + // Update + fun updateShiftDataIntoDatabase( + id: Long, + typeString: String, + descriptionString: String, + dateString: String, + timeInString: String, + timeOutString: String, + duration: Float, + breaks: Int, + units: Float, + payRate: Float, + totalPay: Float, + ): Int { + val itemUri: Uri = ContentUris.withAppendedId(CONTENT_URI, id) + + val values = ContentValues().apply { + put(COLUMN_SHIFT_TYPE, typeString) + put(COLUMN_SHIFT_DESCRIPTION, descriptionString) + put(COLUMN_SHIFT_DATE, dateString) + put(COLUMN_SHIFT_TIME_IN, timeInString) + put(COLUMN_SHIFT_TIME_OUT, timeOutString) + put(COLUMN_SHIFT_DURATION, duration) + put(COLUMN_SHIFT_BREAK, breaks) + put(COLUMN_SHIFT_UNIT, units) + put(COLUMN_SHIFT_PAYRATE, payRate) + put(COLUMN_SHIFT_TOTALPAY, totalPay) + } + return resolver.update(itemUri, values, null, null) + } + + // Delete + fun deleteAllShiftsInDatabase(): Int { + return resolver.delete(CONTENT_URI, null, null) + } + + fun deleteSingleShift(id: Long): Int { + val args: Array = arrayOf(id.toString()) + return resolver.delete(CONTENT_URI, "$_ID=?", args) + } + + private fun Cursor.getShift(): ShiftObject = run { + val id = getLong(getColumnIndexOrThrow(_ID)) + val descriptionColumnIndex = getString( + getColumnIndexOrThrow( + COLUMN_SHIFT_DESCRIPTION + ) + ) + val dateColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_DATE)) + val timeInColumnIndex = + getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_IN)) + val timeOutColumnIndex = + getString(getColumnIndexOrThrow(COLUMN_SHIFT_TIME_OUT)) + val durationColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_DURATION)) + val breakOutColumnIndex = + getInt(getColumnIndexOrThrow(COLUMN_SHIFT_BREAK)) + val typeColumnIndex = getString(getColumnIndexOrThrow(COLUMN_SHIFT_TYPE)) + val unitColumnIndex = getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_UNIT)) + val payrateColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_PAYRATE)) + val totalpayColumnIndex = + getFloat(getColumnIndexOrThrow(COLUMN_SHIFT_TOTALPAY)) + + ShiftObject( + id = id, + type = typeColumnIndex, + description = descriptionColumnIndex, + date = dateColumnIndex, + timeIn = timeInColumnIndex, + timeOut = timeOutColumnIndex, + duration = durationColumnIndex, + breakMins = breakOutColumnIndex, + units = unitColumnIndex, + rateOfPay = payrateColumnIndex, + totalPay = totalpayColumnIndex + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt new file mode 100644 index 0000000..af40553 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftObject.kt @@ -0,0 +1,28 @@ +package com.appttude.h_mal.farmr.data.legacydb + +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import kotlin.math.floor + +data class ShiftObject( + val id: Long, + val type: String, + val description: String, + val date: String, + val timeIn: String, + val timeOut: String, + val duration: Float, + val breakMins: Int, + val units: Float, + val rateOfPay: Float, + val totalPay: Float +) { + fun copyToShift() = Shift(ShiftType.getEnumByType(type), description, date, timeIn, timeOut, duration, breakMins, units, rateOfPay, totalPay) + + fun getHoursMinutesPairFromDuration(): Pair { + val hours: Int = floor(duration).toInt() + val minutes: Int = ((duration - hours) * 60).toInt() + return Pair(hours.toString(), minutes.toString()) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt similarity index 53% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt index d6cc534..727fe1f 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftProvider.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftProvider.kt @@ -1,15 +1,13 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentProvider import android.content.ContentUris import android.content.ContentValues -import android.content.Context import android.content.UriMatcher import android.database.Cursor import android.net.Uri import android.util.Log -import androidx.annotation.VisibleForTesting -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry /** * Created by h_mal on 26/12/2017. @@ -21,22 +19,24 @@ class ShiftProvider : ContentProvider() { return true } - override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, - sortOrder: String?): Cursor? { - var selection = selection - var selectionArgs = selectionArgs + override fun query( + uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, + sortOrder: String? + ): Cursor { val database = mDbHelper!!.readableDatabase - val cursor: Cursor - val match = sUriMatcher.match(uri) - when (match) { - SHIFTS -> cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder) + val cursor: Cursor = when (sUriMatcher.match(uri)) { + SHIFTS -> database.query( + ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, + null, null, sortOrder + ) SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - cursor = database.query(ShiftsEntry.TABLE_NAME, projection, selection, selectionArgs, - null, null, sortOrder) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + database.query( + ShiftsEntry.TABLE_NAME, projection, mSelection, mSelectionArgs, + null, null, sortOrder + ) } else -> throw IllegalArgumentException("Cannot query $uri") @@ -46,26 +46,25 @@ class ShiftProvider : ContentProvider() { } override fun insert(uri: Uri, contentValues: ContentValues?): Uri? { - val match = sUriMatcher.match(uri) - return when (match) { + return when (sUriMatcher.match(uri)) { SHIFTS -> insertShift(uri, contentValues) else -> throw IllegalArgumentException("Insertion is not supported for $uri") } } private fun insertShift(uri: Uri, values: ContentValues?): Uri? { - val description = values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - ?: throw IllegalArgumentException("Description required") - val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) - ?: throw IllegalArgumentException("Date required") - val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - ?: throw IllegalArgumentException("Time In required") - val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - ?: throw IllegalArgumentException("Time Out required") + values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) + ?: throw IllegalArgumentException("Description required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) + ?: throw IllegalArgumentException("Date required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) + ?: throw IllegalArgumentException("Time In required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) + ?: throw IllegalArgumentException("Time Out required") val duration = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_DURATION) require(duration >= 0) { "Duration cannot be negative" } - val shiftType = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE) - ?: throw IllegalArgumentException("Shift type required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TYPE) + ?: throw IllegalArgumentException("Shift type required") val shiftUnits = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_UNIT) require(shiftUnits >= 0) { "Units cannot be negative" } val payRate = values.getAsFloat(ShiftsEntry.COLUMN_SHIFT_PAYRATE) @@ -84,43 +83,47 @@ class ShiftProvider : ContentProvider() { return ContentUris.withAppendedId(uri, id) } - override fun update(uri: Uri, contentValues: ContentValues?, selection: String?, - selectionArgs: Array?): Int { - var selection = selection - var selectionArgs = selectionArgs - val match = sUriMatcher.match(uri) - return when (match) { + override fun update( + uri: Uri, contentValues: ContentValues?, selection: String?, + selectionArgs: Array? + ): Int { + return when (sUriMatcher.match(uri)) { SHIFTS -> updateShift(uri, contentValues, selection, selectionArgs) SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - updateShift(uri, contentValues, selection, selectionArgs) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + updateShift(uri, contentValues, mSelection, mSelectionArgs) } else -> throw IllegalArgumentException("Update is not supported for $uri") } } - private fun updateShift(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int { + private fun updateShift( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { if (values!!.containsKey(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) { - val description = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) - ?: throw IllegalArgumentException("description required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION) + ?: throw IllegalArgumentException("description required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_DATE)) { - val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) - ?: throw IllegalArgumentException("date required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE) + ?: throw IllegalArgumentException("date required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_IN)) { - val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) - ?: throw IllegalArgumentException("time in required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN) + ?: throw IllegalArgumentException("time in required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)) { - val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) - ?: throw IllegalArgumentException("time out required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT) + ?: throw IllegalArgumentException("time out required") } if (values.containsKey(ShiftsEntry.COLUMN_SHIFT_BREAK)) { - val breaks = values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK) - ?: throw IllegalArgumentException("break required") + values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK) + ?: throw IllegalArgumentException("break required") } if (values.size() == 0) { return 0 @@ -134,17 +137,15 @@ class ShiftProvider : ContentProvider() { } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - var selection = selection - var selectionArgs = selectionArgs val database = mDbHelper!!.writableDatabase - val rowsDeleted: Int - val match = sUriMatcher.match(uri) - when (match) { - SHIFTS -> rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + val rowsDeleted: Int = when (sUriMatcher.match(uri)) { + SHIFTS -> database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + SHIFT_ID -> { - selection = ShiftsEntry._ID + "=?" - selectionArgs = arrayOf(ContentUris.parseId(uri).toString()) - rowsDeleted = database.delete(ShiftsEntry.TABLE_NAME, selection, selectionArgs) + val mSelection = ShiftsEntry._ID + "=?" + val mSelectionArgs = arrayOf(ContentUris.parseId(uri).toString()) + + database.delete(ShiftsEntry.TABLE_NAME, mSelection, mSelectionArgs) } else -> throw IllegalArgumentException("Deletion is not supported for $uri") @@ -171,7 +172,11 @@ class ShiftProvider : ContentProvider() { init { sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS, SHIFTS) - sUriMatcher.addURI(ShiftsContract.CONTENT_AUTHORITY, ShiftsContract.PATH_SHIFTS + "/#", SHIFT_ID) + sUriMatcher.addURI( + ShiftsContract.CONTENT_AUTHORITY, + ShiftsContract.PATH_SHIFTS + "/#", + SHIFT_ID + ) } } diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt similarity index 96% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt index ac1a5d0..9fa0a96 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsContract.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsContract.kt @@ -1,4 +1,4 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.ContentResolver import android.net.Uri diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt similarity index 95% rename from app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt rename to app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt index fe83ed6..4918518 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/data/ShiftsDbHelper.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/legacydb/ShiftsDbHelper.kt @@ -1,9 +1,9 @@ -package com.appttude.h_mal.farmr.data +package com.appttude.h_mal.farmr.data.legacydb import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper -import com.appttude.h_mal.farmr.data.ShiftsContract.ShiftsEntry +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry /** * Created by h_mal on 26/12/2017. diff --git a/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt new file mode 100644 index 0000000..11481c1 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/data/prefs/PreferencesProvider.kt @@ -0,0 +1,69 @@ +package com.appttude.h_mal.farmr.data.prefs + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable + +/** + * Shared preferences to save & load last timestamp + */ +const val SORT = "SORT" +const val ORDER = "ORDER" + +const val DESCRIPTION = "DESCRIPTION" +const val DATE_IN = "TIME_IN" +const val DATE_OUT = "TIME_OUT" +const val TYPE = "TYPE" + +class PreferenceProvider( + context: Context +) { + + private val appContext = context.applicationContext + + private val preference: SharedPreferences + get() = PreferenceManager.getDefaultSharedPreferences(appContext) + + fun saveSortableAndOrder(sortable: Sortable, order: Order) { + preference.edit() + .putString(SORT, sortable.label) + .putString(ORDER, order.label) + .apply() + } + + fun getSortableAndOrder(): Pair { + val sort = preference.getString(SORT, null)?.let { Sortable.valueOf(it) } + val order = preference.getString(ORDER, null)?.let { Order.valueOf(it) } + + return Pair(sort, order) + } + fun saveFilteringDetails( + description: String?, + timeIn: String?, + timeOut: String?, + type: String? + ) { + preference.edit() + .putString(DESCRIPTION, description) + .putString(DATE_IN, timeIn) + .putString(DATE_OUT, timeOut) + .putString(TYPE, type) + .apply() + } + + fun getFilteringDetails(): Map { + return mapOf( + Pair(DESCRIPTION, preference.getString(DESCRIPTION, null)), + Pair(DATE_IN, preference.getString(DATE_IN, null)), + Pair(DATE_OUT, preference.getString(DATE_OUT, null)), + Pair(TYPE, preference.getString(TYPE, null)) + ) + } + + fun clearPrefs() { + preference.edit().clear().apply() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt new file mode 100644 index 0000000..cc4b998 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/di/ShiftApplication.kt @@ -0,0 +1,15 @@ +package com.appttude.h_mal.farmr.di + +import com.appttude.h_mal.farmr.base.BaseApplication +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider + +class ShiftApplication: BaseApplication() { + + override fun createDatabase(): LegacyDatabase { + return LegacyDatabase(contentResolver) + } + + override fun createPrefs() = PreferenceProvider(this) +} + diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt new file mode 100644 index 0000000..4120cdc --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/DatabaseShift.kt @@ -0,0 +1,11 @@ +package com.appttude.h_mal.farmr.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity +data class EntityItem( + @PrimaryKey(autoGenerate = false) + val id: String, + val shift: Shift +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt new file mode 100644 index 0000000..45a3e87 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/FilterStore.kt @@ -0,0 +1,8 @@ +package com.appttude.h_mal.farmr.model + +data class FilterStore( + val description: String?, + val dateFrom: String?, + val dateTo: String?, + val type: String? +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt new file mode 100644 index 0000000..b7c971f --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Order.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.model + +enum class Order(val label: String) { + ASCENDING("Ascending"), DESCENDING("Descending") +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt index 5d76543..4df1de5 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Shift.kt @@ -1,14 +1,65 @@ package com.appttude.h_mal.farmr.model +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.formatToTwoDp + data class Shift( - val type: ShiftType, - val description: String, - val date: String, - val timeIn: String?, - val timeOut: String?, - val duration: Float?, - val breakMins: Int?, - val units: Float?, - val rateOfPay: Float, - val totalPay: Float -) \ No newline at end of file + val type: ShiftType, + val description: String, + val date: String, + val timeIn: String?, + val timeOut: String?, + val duration: Float?, + val breakMins: Int?, + val units: Float?, + val rateOfPay: Float, + val totalPay: Float +) { + companion object { + // Invocation for Hourly + operator fun invoke( + description: String, + date: String, + timeIn: String, + timeOut: String, + breakMins: Int? = null, + rateOfPay: Float + ): Shift { + val breakTime = breakMins ?: 0 + val duration = calculateDuration(timeIn, timeOut, breakTime) + + return Shift( + ShiftType.HOURLY, + description, + date, + timeIn, + timeOut, + duration, + breakTime, + 0f, + rateOfPay, + (duration * rateOfPay).formatToTwoDp() + ) + } + + operator fun invoke( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) = Shift( + ShiftType.PIECE, + description, + date, + "", + "", + 0f, + 0, + units, + rateOfPay, + (units * rateOfPay).formatToTwoDp() + ) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt index d4d1118..4087686 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ShiftType.kt @@ -1,6 +1,12 @@ package com.appttude.h_mal.farmr.model -enum class ShiftType(val type: String) { +enum class ShiftType(val type: String){ HOURLY("Hourly"), - PIECE("Piece Rate") + PIECE("Piece Rate"); + + companion object { + fun getEnumByType(type: String): ShiftType { + return values().first { it.type == type } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt new file mode 100644 index 0000000..bb2bbf7 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Sortable.kt @@ -0,0 +1,17 @@ +package com.appttude.h_mal.farmr.model + +enum class Sortable(val label: String) { + ID("Default"), + TYPE("Shift Type"), + DATE("Date"), + DESCRIPTION("Description"), + DURATION("Added"), UNITS("Duration"), + RATEOFPAY("Rate of pay"), + TOTALPAY("Total Pay"); + + companion object { + fun getEnumByType(label: String): Sortable { + return Sortable.values().first { it.label == label } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt new file mode 100644 index 0000000..b7b3d9d --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/Success.kt @@ -0,0 +1,5 @@ +package com.appttude.h_mal.farmr.model + +data class Success( + val successMessage: String +) \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt new file mode 100644 index 0000000..2080f55 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/model/ViewState.kt @@ -0,0 +1,7 @@ +package com.appttude.h_mal.farmr.model + +sealed class ViewState { + object HasStarted : ViewState() + class HasData(val data: T) : ViewState() + class HasError(val error: T) : ViewState() +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt new file mode 100644 index 0000000..400c5a4 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FilterDataFragment.kt @@ -0,0 +1,105 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.view.View.OnClickListener +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.Button +import android.widget.EditText +import android.widget.Spinner +import androidx.core.widget.doAfterTextChanged +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.setDatePicker +import com.appttude.h_mal.farmr.viewmodel.FilterViewModel + +class FilterDataFragment : BaseFragment(R.layout.fragment_filter_data), + AdapterView.OnItemSelectedListener, OnClickListener { + private val spinnerList: Array = + arrayOf("", ShiftType.HOURLY.type, ShiftType.PIECE.type) + + private lateinit var LocationET: EditText + private lateinit var dateFromET: EditText + private lateinit var dateToET: EditText + private lateinit var typeSpinner: Spinner + + private var descriptionString: String? = null + private var dateFromString: String? = null + private var dateToString: String? = null + private var typeString: String? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTitle(getString(R.string.title_activity_filter_data)) + + LocationET = view.findViewById(R.id.filterLocationEditText) + dateFromET = view.findViewById(R.id.fromdateInEditText) + dateToET = view.findViewById(R.id.filterDateOutEditText) + typeSpinner = view.findViewById(R.id.TypeFilterEditText) + val submit: Button = view.findViewById(R.id.submitFiltered) + + val adapter: ArrayAdapter = + ArrayAdapter((context)!!, android.R.layout.simple_spinner_dropdown_item, spinnerList) + typeSpinner.adapter = adapter + + val filterDetails = viewModel.getFiltrationDetails() + + filterDetails.run { + description?.let { + LocationET.setText(it) + descriptionString = it + } + dateFrom?.let { + dateFromET.setText(it) + dateFromString = it + } + dateTo?.let { + dateToET.setText(it) + dateToString = it + } + type?.let { + typeString = it + val spinnerPosition: Int = adapter.getPosition(it) + typeSpinner.setSelection(spinnerPosition) + } + } + + LocationET.doAfterTextChanged { descriptionString = it.toString() } + dateFromET.setDatePicker { dateFromString = it } + dateToET.setDatePicker { dateToString = it } + typeSpinner.onItemSelectedListener = this + + submit.setOnClickListener(this) + } + + override fun onItemSelected( + parentView: AdapterView<*>?, + selectedItemView: View?, + position: Int, + id: Long + ) { + typeString = when (position) { + 1 -> ShiftType.HOURLY.type + 2 -> ShiftType.PIECE.type + else -> return + } + } + + override fun onNothingSelected(parentView: AdapterView<*>?) {} + + private fun submitFiltrationDetails() { + viewModel.applyFilters(descriptionString, dateFromString, dateToString, typeString) + } + + override fun onClick(p0: View?) { + submitFiltrationDetails() + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) popBackStack() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt new file mode 100644 index 0000000..1b9caf5 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentAddItem.kt @@ -0,0 +1,298 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.RadioButton +import android.widget.RadioGroup +import android.widget.ScrollView +import android.widget.TextView +import androidx.core.widget.doAfterTextChanged +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString +import com.appttude.h_mal.farmr.utils.formatToTwoDpString +import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.popBackStack +import com.appttude.h_mal.farmr.utils.setDatePicker +import com.appttude.h_mal.farmr.utils.setTimePicker +import com.appttude.h_mal.farmr.utils.show +import com.appttude.h_mal.farmr.utils.validateField +import com.appttude.h_mal.farmr.viewmodel.SubmissionViewModel + +class FragmentAddItem : BaseFragment(R.layout.fragment_add_item), + RadioGroup.OnCheckedChangeListener, BackPressedListener { + + private lateinit var mHourlyRadioButton: RadioButton + private lateinit var mPieceRadioButton: RadioButton + private lateinit var mLocationEditText: EditText + private lateinit var mDateEditText: EditText + private lateinit var mDurationTextView: TextView + private lateinit var mTimeInEditText: EditText + private lateinit var mTimeOutEditText: EditText + private lateinit var mBreakEditText: EditText + private lateinit var mUnitEditText: EditText + private lateinit var mPayRateEditText: EditText + private lateinit var mTotalPayTextView: TextView + private lateinit var hourlyDataView: LinearLayout + private lateinit var unitsHolder: LinearLayout + private lateinit var durationHolder: LinearLayout + private lateinit var wholeView: LinearLayout + private lateinit var scrollView: ScrollView + private lateinit var submitProduct: Button + private lateinit var mRadioGroup: RadioGroup + + private var mDate: String? = null + private var mDescription: String? = null + private var mTimeIn: String? = null + private var mTimeOut: String? = null + private var mBreaks: Int? = null + private var mUnits: Float? = null + private var mPayRate = 0f + private var mType: ShiftType? = null + private var mDuration: Float? = null + + private var id: Long? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + scrollView = view.findViewById(R.id.total_view) + mRadioGroup = view.findViewById(R.id.rg) + mHourlyRadioButton = view.findViewById(R.id.hourly) + mPieceRadioButton = view.findViewById(R.id.piecerate) + mLocationEditText = view.findViewById(R.id.locationEditText) + mDateEditText = view.findViewById(R.id.dateEditText) + mTimeInEditText = view.findViewById(R.id.timeInEditText) + mBreakEditText = view.findViewById(R.id.breakEditText) + mTimeOutEditText = view.findViewById(R.id.timeOutEditText) + mDurationTextView = view.findViewById(R.id.ShiftDuration) + mUnitEditText = view.findViewById(R.id.unitET) + mPayRateEditText = view.findViewById(R.id.payrateET) + mTotalPayTextView = view.findViewById(R.id.totalpayval) + hourlyDataView = view.findViewById(R.id.hourly_data_holder) + unitsHolder = view.findViewById(R.id.units_holder) + durationHolder = view.findViewById(R.id.duration_holder) + wholeView = view.findViewById(R.id.whole_view) + submitProduct = view.findViewById(R.id.submit) + + mRadioGroup.setOnCheckedChangeListener(this) + mLocationEditText.doAfterTextChanged { + mDescription = it.toString() + } + mDateEditText.setDatePicker { mDate = it } + mTimeInEditText.setTimePicker { + mTimeIn = it + calculateTotalPay() + } + mTimeOutEditText.setTimePicker { + mTimeOut = it + calculateTotalPay() + } + mBreakEditText.doAfterTextChanged { + mBreaks = it.toString().toIntOrNull() ?: 0 + calculateTotalPay() + } + mUnitEditText.doAfterTextChanged { + it.toString().toFloatOrNull()?.let { u -> mUnits = u } + calculateTotalPay() + } + mPayRateEditText.doAfterTextChanged { + it.toString().toFloatOrNull()?.let { p -> + mPayRate = p + calculateTotalPay() + } + } + + submitProduct.setOnClickListener { submitShift() } + + setupViewAfterViewCreated() + } + + private fun setupViewAfterViewCreated() { + id = arguments?.getLong(ID) + wholeView.hide() + + val title = when (arguments?.containsKey(ID)) { + true -> { + // Since we are editing a shift lets load the shift data into the views + viewModel.getCurrentShift(arguments!!.getLong(ID))?.run { + mLocationEditText.setText(description) + mDateEditText.setText(date) + + // Set types + mType = ShiftType.getEnumByType(type) + mDescription = description + mDate = date + mPayRate = rateOfPay + + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + mHourlyRadioButton.isChecked = true + mPieceRadioButton.isChecked = false + mTimeInEditText.setText(timeIn) + mTimeOutEditText.setText(timeOut) + mBreakEditText.setText(breakMins.toString()) + val durationText = "${duration.formatToTwoDpString()} Hours" + mDurationTextView.text = durationText + + // Set fields + mTimeIn = timeIn + mTimeOut = timeOut + mBreaks = breakMins + } + + ShiftType.PIECE -> { + mHourlyRadioButton.isChecked = false + mPieceRadioButton.isChecked = true + mUnitEditText.setText(units.formatToTwoDpString()) + + // Set piece rate units + mUnits = units + } + } + mPayRateEditText.setText(rateOfPay.formatAsCurrencyString()) + mTotalPayTextView.text = totalPay.formatAsCurrencyString() + + calculateTotalPay() + } + + // Return title + getString(R.string.edit_item_title) + } + + else -> getString(R.string.add_item_title) + } + setTitle(title) + } + + override fun onCheckedChanged(radioGroup: RadioGroup, id: Int) { + when (radioGroup.checkedRadioButtonId) { + R.id.hourly -> { + mType = ShiftType.HOURLY + wholeView.show() + unitsHolder.hide() + hourlyDataView.show() + durationHolder.show() + } + + R.id.piecerate -> { + mType = ShiftType.PIECE + wholeView.show() + unitsHolder.show() + hourlyDataView.hide() + durationHolder.hide() + } + } + } + + private fun submitShift() { + mDate.validateField({ !it.isNullOrBlank() }) { + onFailure("Date field cannot be empty") + return + } + mDescription.validateField({ !it.isNullOrBlank() }) { + onFailure("Description field cannot be empty") + return + } + mPayRate.validateField({ !it.isNaN() }) { + onFailure("Rate of pay field cannot be empty") + return + } + + if (mPieceRadioButton.isChecked) { + + mUnits.validateField({ it != null && it >= 0 }) { + onFailure("Units field cannot be empty") + return + } + if (id != null) { + // update + viewModel.updateShift( + id!!, + description = mDescription, + date = mDate, + units = mUnits, + rateOfPay = mPayRate + ) + } else { + // insert + viewModel.insertPieceRateShift(mDescription!!, mDate!!, mUnits!!, mPayRate) + } + } else if (mHourlyRadioButton.isChecked) { + if (id != null) { + // update + viewModel.updateShift( + id!!, + description = mDescription, + date = mDate, + rateOfPay = mPayRate, + timeIn = mTimeIn, + timeOut = mTimeOut, + breakMins = mBreaks + ) + } else { + // insert + viewModel.insertHourlyShift( + mDescription!!, + mDate!!, + mPayRate, + mTimeIn, + mTimeOut, + mBreaks + ) + } + + } + } + + private fun calculateTotalPay() { + mType?.let { + val total = when (it) { + ShiftType.HOURLY -> { + // Calculate duration before total pay calculation + mDuration = viewModel.retrieveDurationText(mTimeIn, mTimeOut, mBreaks) ?: return + mDurationTextView.text = + StringBuilder().append(mDuration).append(" hours").toString() + mDuration!! * mPayRate + } + ShiftType.PIECE -> { + (mUnits ?: 0f) * mPayRate + } + } + mTotalPayTextView.text = total.formatAsCurrencyString() + } + } + + override fun onBackPressed(): Boolean { + if (mRadioGroup.checkedRadioButtonId == -1) { + mActivity?.popBackStack() + } else { + requireContext().createDialog( + title = "Discard Changes?", + message = "Are you sure you want to discard changes?", + displayCancel = true, + okCallback = { _, _ -> + mActivity?.popBackStack() + } + ) + } + return true + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is Success) { + displayToast(data.successMessage) + popBackStack() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt new file mode 100644 index 0000000..5e1866b --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FragmentMain.kt @@ -0,0 +1,260 @@ +package com.appttude.h_mal.farmr.ui + +import android.Manifest +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.MenuItem +import android.view.View +import android.widget.Toast +import androidx.core.app.ActivityCompat +import androidx.core.content.FileProvider +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.AdapterDataObserver +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.createDialog +import com.appttude.h_mal.farmr.utils.displayToast +import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.utils.show +import com.appttude.h_mal.farmr.viewmodel.MainViewModel +import com.google.android.material.floatingactionbutton.FloatingActionButton +import java.io.File +import kotlin.system.exitProcess + + +class FragmentMain : BaseFragment(R.layout.fragment_main), BackPressedListener { + private lateinit var productListView: RecyclerView + private lateinit var emptyView: View + private lateinit var mAdapter: ShiftListAdapter + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTitle("Shift List") + // Inflate the layout for this fragment + setHasOptionsMenu(true) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + mAdapter = ShiftListAdapter(this) { + viewModel.deleteShift(it) + } + productListView = view.findViewById(R.id.list_item_view) + productListView.adapter = mAdapter + emptyView = view.findViewById(R.id.empty_view) + + mAdapter.registerAdapterDataObserver(object : AdapterDataObserver() { + override fun onChanged() { + super.onChanged() + if (mAdapter.itemCount == 0) emptyView.show() + else emptyView.hide() + } + }) + + view.findViewById(R.id.fab1).setOnClickListener { + navigateToFragment(FragmentAddItem(), name = "additem") + } + } + + override fun onStart() { + super.onStart() + viewModel.refreshLiveData() + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is List<*>) { + @Suppress("UNCHECKED_CAST") + mAdapter.submitList(data as List) + } + if (data is Success) { + displayToast(data.successMessage) + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.delete_all -> { + deleteAllProducts() + return true + } + + R.id.help -> { + AlertDialog.Builder(context) + .setTitle("Help & Support:") + .setView(R.layout.dialog_layout) + .setPositiveButton(android.R.string.ok) { arg0, _ -> arg0.dismiss() } + .create().show() + return true + } + + R.id.filter_data -> { + navigateToFragment(FilterDataFragment(), name = "filterdata") + return true + } + + R.id.sort_data -> { + sortData() + return true + } + + R.id.clear_filter -> { + viewModel.clearFilters() + return true + } + + R.id.export_data -> { + if (checkStoragePermissions(activity)) { + AlertDialog.Builder(context) + .setTitle("Export?") + .setMessage("Exporting current filtered data. Continue?") + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> exportData() } + .create().show() + } else { + displayToast("Storage permissions required") + } + return true + } + + R.id.action_favorite -> { + AlertDialog.Builder(context) + .setTitle("Info:") + .setMessage(viewModel.getInformation()) + .setPositiveButton(android.R.string.ok) { arg0, _ -> + arg0.dismiss() + }.create().show() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun sortData() { + val groupName = Sortable.values().map { it.label }.toTypedArray() + var sort = Sortable.ID + + val sortAndOrder = viewModel.getSortAndOrder() + val checkedItem = Sortable.values().indexOf(sortAndOrder.first) + + AlertDialog.Builder(context) + .setTitle("Sort by:") + .setSingleChoiceItems( + groupName, + checkedItem + ) { _, p1 -> sort = Sortable.getEnumByType(groupName[p1]) } + .setPositiveButton("Ascending") { dialog, _ -> + viewModel.setSortAndOrder(sort) + dialog.dismiss() + }.setNegativeButton("Descending") { dialog, _ -> + viewModel.setSortAndOrder(sort, Order.DESCENDING) + dialog.dismiss() + } + .create().show() + } + + private fun deleteAllProducts() { + requireContext().createDialog( + "Warning", + message = "Are you sure you want to delete all date?", + displayCancel = true, + okCallback = { _, _ -> + viewModel.deleteAllShifts() + } + ) + } + + private fun exportData() { + val permission = + ActivityCompat.checkSelfPermission(requireActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE) + if (permission != PackageManager.PERMISSION_GRANTED) { + Toast.makeText(context, "Storage permissions not granted", Toast.LENGTH_SHORT).show() + return + } + + val fileName = "shifthistory.xls" + val file = File(requireContext().externalCacheDir, fileName) + + viewModel.createExcelSheet(file)?.let { + val intent = Intent(Intent.ACTION_VIEW) + val excelUri = FileProvider.getUriForFile( + requireContext(), + requireContext().applicationContext.packageName + ".provider", + file + ) + intent.setDataAndType(excelUri, "application/vnd.ms-excel") + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + startActivity(intent) + } + + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + println("request code$requestCode") + if (requestCode == MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE) { + if (grantResults.isNotEmpty() + && grantResults[0] == PackageManager.PERMISSION_GRANTED + ) { + exportDialog() + } else { + displayToast("Storage Permissions denied") + } + } + } + + private fun exportDialog() { + AlertDialog.Builder(context) + .setTitle("Export?") + .setMessage("Exporting current filtered data. Continue?") + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> exportData() }.create().show() + } + + private fun checkStoragePermissions(activity: Activity?): Boolean { + var status = false + val permission = ActivityCompat.checkSelfPermission( + activity!!, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + if (permission == PackageManager.PERMISSION_GRANTED) { + status = true + } + return status + } + + companion object { + const val MY_PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1 + } + + override fun onBackPressed(): Boolean { + requireContext().createDialog( + title = "Leave?", + message = "Are you sure you want to exit Farmr?", + displayCancel = true, + okCallback = { _, _ -> + val intent = Intent(Intent.ACTION_MAIN) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + intent.addCategory(Intent.CATEGORY_HOME) + startActivity(intent) + requireActivity().finish() + exitProcess(0) + } + ) + return true + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt new file mode 100644 index 0000000..be79fa7 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/FurtherInfoFragment.kt @@ -0,0 +1,101 @@ +package com.appttude.h_mal.farmr.ui + +import android.os.Bundle +import android.view.View +import android.widget.Button +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.TextView +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseFragment +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.CURRENCY +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString +import com.appttude.h_mal.farmr.utils.hide +import com.appttude.h_mal.farmr.utils.navigateToFragment +import com.appttude.h_mal.farmr.utils.show +import com.appttude.h_mal.farmr.viewmodel.InfoViewModel + +class FurtherInfoFragment : BaseFragment(R.layout.fragment_futher_info) { + private lateinit var typeTV: TextView + private lateinit var descriptionTV: TextView + private lateinit var dateTV: TextView + private lateinit var times: TextView + private lateinit var breakTV: TextView + private lateinit var durationTV: TextView + private lateinit var unitsTV: TextView + private lateinit var payRateTV: TextView + private lateinit var totalPayTV: TextView + private lateinit var hourlyDetailHolder: LinearLayout + private lateinit var unitsHolder: LinearLayout + private lateinit var wholeView: LinearLayout + private lateinit var progressBarFI: ProgressBar + private lateinit var editButton: Button + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setTitle(getString(R.string.further_info_title)) + + progressBarFI = view.findViewById(R.id.progressBar_info) + wholeView = view.findViewById(R.id.further_info_view) + typeTV = view.findViewById(R.id.details_shift) + descriptionTV = view.findViewById(R.id.details_desc) + dateTV = view.findViewById(R.id.details_date) + times = view.findViewById(R.id.details_time) + breakTV = view.findViewById(R.id.details_breaks) + durationTV = view.findViewById(R.id.details_duration) + unitsTV = view.findViewById(R.id.details_units) + payRateTV = view.findViewById(R.id.details_pay_rate) + totalPayTV = view.findViewById(R.id.details_totalpay) + editButton = view.findViewById(R.id.details_edit) + hourlyDetailHolder = view.findViewById(R.id.details_hourly_details) + unitsHolder = view.findViewById(R.id.details_units_holder) + + editButton.setOnClickListener { + navigateToFragment(FragmentAddItem(), name = "additem", bundle = arguments!!) + } + + viewModel.retrieveData(arguments) + } + + override fun onSuccess(data: Any?) { + super.onSuccess(data) + if (data is ShiftObject) data.setupView() + } + + private fun ShiftObject.setupView() { + typeTV.text = type + descriptionTV.text = description + dateTV.text = date + payRateTV.text = rateOfPay.toString() + totalPayTV.text = StringBuilder(CURRENCY).append(totalPay).toString() + + when (ShiftType.getEnumByType(type)) { + ShiftType.HOURLY -> { + hourlyDetailHolder.show() + unitsHolder.hide() + times.text = StringBuilder(timeIn).append("-").append(timeOut).toString() + breakTV.text = StringBuilder().append(breakMins).append(" mins").toString() + durationTV.text = viewModel.buildDurationSummary(this) + val paymentSummary = + StringBuilder().append(duration).append(" Hours @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Hour").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary + } + + ShiftType.PIECE -> { + hourlyDetailHolder.hide() + unitsHolder.show() + unitsTV.text = units.toString() + + val paymentSummary = + StringBuilder().append(units.formatAsCurrencyString()).append(" Units @ ") + .append(rateOfPay.formatAsCurrencyString()).append(" per Unit").append("\n") + .append("Equals: ").append(totalPay.formatAsCurrencyString()) + totalPayTV.text = paymentSummary + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt new file mode 100644 index 0000000..2bb2763 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/MainActivity.kt @@ -0,0 +1,78 @@ +package com.appttude.h_mal.farmr.ui + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.Menu +import androidx.appcompat.widget.Toolbar +import androidx.core.app.ActivityCompat +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BackPressedListener +import com.appttude.h_mal.farmr.base.BaseActivity +import com.appttude.h_mal.farmr.utils.popBackStack + +class MainActivity : BaseActivity() { + private lateinit var toolbar: Toolbar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.main_view) + toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + + verifyStoragePermissions(this) + + val fragmentTransaction = supportFragmentManager.beginTransaction() + fragmentTransaction.replace(R.id.container, FragmentMain()).addToBackStack("main").commit() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + // Inflate the menu; this adds items to the action bar if it is present. + menuInflater.inflate(R.menu.menu_main, menu) + return true + } + + override fun onBackPressed() { + val currentFragment = supportFragmentManager.findFragmentById(R.id.container) + if (currentFragment is BackPressedListener) { + currentFragment.onBackPressed() + } else { + if (supportFragmentManager.backStackEntryCount > 1) { + popBackStack() + } else { + super.onBackPressed() + } + } + } + + // Storage Permissions + private val REQUEST_EXTERNAL_STORAGE = 1 + private val PERMISSIONS_STORAGE = arrayOf( + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + + /** + * Checks if the app has permission to write to device storage + * + * If the app does not has permission then the user will be prompted to grant permissions + * + * @param activity + */ + fun verifyStoragePermissions(activity: Activity?) { + // Check if we have write permission + val permission = ActivityCompat.checkSelfPermission( + activity!!, + Manifest.permission.WRITE_EXTERNAL_STORAGE + ) + if (permission != PackageManager.PERMISSION_GRANTED) { + // We don't have permission so prompt the user + ActivityCompat.requestPermissions( + activity, + PERMISSIONS_STORAGE, + REQUEST_EXTERNAL_STORAGE + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt new file mode 100644 index 0000000..5151661 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/ShiftListAdapter.kt @@ -0,0 +1,115 @@ +package com.appttude.h_mal.farmr.ui + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.os.Bundle +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.appttude.h_mal.farmr.R +import com.appttude.h_mal.farmr.base.BaseRecyclerAdapter +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.utils.ID +import com.appttude.h_mal.farmr.utils.generateView +import com.appttude.h_mal.farmr.utils.navigateToFragment + +class ShiftListAdapter( + private val fragment: Fragment, + private val longPressCallback: (Long) -> Unit +) : ListAdapter(diffCallBack) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BaseRecyclerAdapter.CurrentViewHolder { + val currentViewHolder = parent.generateView(R.layout.list_item_1) + return BaseRecyclerAdapter.CurrentViewHolder(currentViewHolder) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: BaseRecyclerAdapter.CurrentViewHolder, position: Int) { + val view = holder.itemView + val data = getItem(position) + + val descriptionTextView: TextView = view.findViewById(R.id.location) + val dateTextView: TextView = view.findViewById(R.id.date) + val totalPay: TextView = view.findViewById(R.id.total_pay) + val hoursView: TextView = view.findViewById(R.id.hours) + val h: TextView = view.findViewById(R.id.h) + val minutesView: TextView = view.findViewById(R.id.minutes) + val m: TextView = view.findViewById(R.id.m) + val editView: ImageView = view.findViewById(R.id.imageView) + h.text = "h" + m.text = "m" + val typeText: String = data.type + val descriptionText: String = data.description + val dateText: String = data.date + val totalPayText: String = data.totalPay.toString() + + descriptionTextView.text = descriptionText + dateTextView.text = dateText + totalPay.text = totalPayText + + when (ShiftType.getEnumByType(typeText)) { + ShiftType.HOURLY -> { + val time = data.getHoursMinutesPairFromDuration() + + hoursView.text = time.first + minutesView.text = time.second + } + + ShiftType.PIECE -> { + val unitsText: String = data.units.toString() + + hoursView.text = unitsText + h.text = "" + minutesView.text = "" + m.text = "pcs" + } + } + + val b: Bundle = Bundle() + b.putLong(ID, data.id) + view.setOnClickListener { + // Navigate to further info + fragment.navigateToFragment( + FurtherInfoFragment(), + bundle = b, + name = "furtherinfo" + ) + } + editView.setOnClickListener { + // Navigate to edit + fragment.navigateToFragment( + FragmentAddItem(), + bundle = b, + name = "additem" + ) + } + view.setOnLongClickListener { + AlertDialog.Builder(it.context) + .setMessage("Are you sure you want to delete") + .setPositiveButton("delete") { _, _ -> longPressCallback.invoke(data.id) } + .setNegativeButton("cancel") { dialog, _ -> + dialog?.dismiss() + } + .create().show() + true + } + } + + companion object { + val diffCallBack = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean { + return oldItem == newItem + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt similarity index 50% rename from app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt rename to app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt index 878fb98..5f8cfd8 100644 --- a/app/src/main/java/com/appttude/h_mal/farmr/SplashScreen.kt +++ b/app/src/main/java/com/appttude/h_mal/farmr/ui/SplashScreen.kt @@ -1,36 +1,32 @@ -package com.appttude.h_mal.farmr +package com.appttude.h_mal.farmr.ui +import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.os.Bundle import android.os.Handler -import android.view.View -import android.widget.RelativeLayout -import androidx.core.app.ActivityOptionsCompat +import android.os.Looper +import com.appttude.h_mal.farmr.R /** * Created by h_mal on 27/06/2017. */ +@SuppressLint("CustomSplashScreen") class SplashScreen : Activity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_splash) - val bundle = ActivityOptionsCompat.makeCustomAnimation(this, R.anim.hyperspace_jump, android.R.anim.fade_out).toBundle() - val relativeLayout = findViewById(R.id.splash_layout) as RelativeLayout + val i = Intent(this@SplashScreen, MainActivity::class.java) i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_CLEAR_TASK) - Handler().postDelayed({ - // This method will be executed once the timer is over - // Start your app main activity -// startActivity(i,bundle); + Handler(Looper.getMainLooper()).postDelayed({ startActivity(i) overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out) - // finish(); - }, SPLASH_TIME_OUT.toLong()) + }, SPLASH_TIME_OUT) } companion object { // Splash screen timer - private const val SPLASH_TIME_OUT = 2000 + const val SPLASH_TIME_OUT: Long = 2000 } } \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt new file mode 100644 index 0000000..59f48f8 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Constants.kt @@ -0,0 +1,7 @@ +package com.appttude.h_mal.farmr.utils + +const val LEGACY = "LEGACY_" +const val DATE_FORMAT = "yyyy-MM-dd" +const val TIME_FORMAT = "hh:mm" +const val ID = "ID" +const val CURRENCY = "£" \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt new file mode 100644 index 0000000..5b007e1 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/Formatting.kt @@ -0,0 +1,97 @@ +package com.appttude.h_mal.farmr.utils + +import java.io.IOException +import java.text.NumberFormat +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Currency +import java.util.Date +import java.util.Locale + +fun String.formatToTwoDp(): Float { + val formattedString = String.format("%.2f", this) + return formattedString.toFloat() +} + +fun Float.formatToTwoDp(): Float { + val formattedString = String.format("%.2f", this) + return formattedString.toFloat() +} + +fun Float.formatAsCurrencyString(): String? { + val format: NumberFormat = NumberFormat.getCurrencyInstance() + format.maximumFractionDigits = 2 + format.currency = Currency.getInstance("GBP") + + return format.format(this) +} + +fun Float.formatToTwoDpString(): String { + return formatToTwoDp().toString() +} + +fun String.dateStringIsValid(): Boolean { + return "([0-9]{4})-([0-9]{2})-([0-9]{2})".toPattern().matcher(this).matches() +} + +fun String.timeStringIsValid(): Boolean { + return "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]\$".toPattern().matcher(this).matches() +} + +fun Calendar.getTimeString(): String { + val format = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()) + return format.format(time) +} + +fun String.convertDateString(format: String = DATE_FORMAT): Date? { + val formatter = SimpleDateFormat(format, Locale.getDefault()) + return formatter.parse(this) +} + +/** + * turns "HH:mm" into an hour and minutes pair + * + * eg: + * @param 13:45 + * @return Pair(13, 45) + */ +fun convertTimeStringToHourMinutesPair(timeString: String): Pair { + val split = timeString.split(":") + if (split.size != 2) throw ArrayIndexOutOfBoundsException() + return Pair(split.first().toInt(), split[1].toInt()) +} + + +/** + * calculate the duration between two 24 hour time strings minus the break in minutes + * + * can also calculate when time to string in past midnight eg: 23:00, 04:45, 30 + * @return 5.75 + */ +fun calculateDuration(timeIn: String, timeOut: String, breaks: Int): Float { + val timeFrom = convertTimeStringToHourMinutesPair(timeIn) + val timeTo = convertTimeStringToHourMinutesPair(timeOut) + + val hoursIn = timeFrom.first + val minutesIn = timeFrom.second + val hoursOut = timeTo.first + val minutesOut = timeTo.second + + var duration: Float = if (hoursOut > hoursIn) { + ((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + } else { + (((hoursOut.toFloat() + (minutesOut.toFloat() / 60)) - (hoursIn.toFloat() + (minutesIn.toFloat() / 60))) + 24) + } + if ((breaks.toFloat() / 60) > duration) throw IOException("Breaks duration cannot be larger than shift duration") + duration -= (breaks.toFloat() / 60) + + return duration.formatToTwoDp() +} + +fun calculateDuration(timeIn: String?, timeOut: String?, breaks: Int?): Float { + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + + return calculateDuration(insertTimeIn, insertTimeOut, breaks ?: 0) +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt new file mode 100644 index 0000000..cb0d32c --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/GenericsUtil.kt @@ -0,0 +1,38 @@ +package com.appttude.h_mal.farmr.utils + +import com.appttude.h_mal.farmr.model.Order +import java.lang.reflect.ParameterizedType +import kotlin.reflect.KClass + +@Suppress("UNCHECKED_CAST") +fun Any.getGenericClassAt(position: Int): KClass = + ((javaClass.genericSuperclass as? ParameterizedType) + ?.actualTypeArguments?.getOrNull(position) as? Class) + ?.kotlin + ?: throw IllegalStateException("Can not find class from generic argument") + +/** + * @param validate when result is false then we trigger + * @param onError + * + * + * @sample + * var s: String? + * i.validate{!i.isNullOrEmpty()} { print("string is empty") } + */ +inline fun T.validateField(validate: (T) -> Boolean, onError: () -> Unit) { + if (!validate.invoke(this)) { + onError.invoke() + } +} + +/** + * Returns a list of all elements sorted according to the specified comparator. In order of ascending or descending + * The sort is stable. It means that equal elements preserve their order relative to each other after sorting. + */ +inline fun > Iterable.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List { + return when (order) { + Order.ASCENDING -> sortedWith(compareBy(selector)) + Order.DESCENDING -> sortedWith(compareByDescending(selector)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt new file mode 100644 index 0000000..1e68035 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/utils/ViewUtils.kt @@ -0,0 +1,177 @@ +package com.appttude.h_mal.farmr.utils + +import android.app.Activity +import android.app.AlertDialog +import android.app.DatePickerDialog +import android.app.TimePickerDialog +import android.content.Context +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.animation.Animation +import android.view.animation.AnimationUtils +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import android.widget.Toast +import androidx.annotation.AnimRes +import androidx.annotation.IdRes +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import com.appttude.h_mal.farmr.R +import java.util.Calendar + +fun View.show() { + this.visibility = View.VISIBLE +} + +fun View.hide() { + this.visibility = View.GONE +} + +fun Context.displayToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_LONG).show() +} + +fun Fragment.displayToast(message: String) { + Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show() +} + +fun ViewGroup.generateView(layoutId: Int): View = LayoutInflater + .from(context) + .inflate(layoutId, this, false) + +fun Fragment.hideKeyboard() { + val imm = context?.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager? + imm?.hideSoftInputFromWindow(view?.windowToken, 0) +} + +fun View.triggerAnimation(@AnimRes id: Int, complete: (View) -> Unit) { + val animation = AnimationUtils.loadAnimation(context, id) + animation.setAnimationListener(object : Animation.AnimationListener { + override fun onAnimationEnd(animation: Animation?) = complete(this@triggerAnimation) + override fun onAnimationStart(a: Animation?) {} + override fun onAnimationRepeat(a: Animation?) {} + }) + startAnimation(animation) +} + +fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "") { + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + fragmentTransaction.replace(container, fragment).addToBackStack(name).commit() +} + +fun Fragment.navigateToFragment(fragment: Fragment, @IdRes container: Int = R.id.container, name: String = "", bundle: Bundle) { + val fragmentTransaction = requireActivity().supportFragmentManager.beginTransaction() + fragmentTransaction.replace(container, fragment.apply { arguments = bundle }).addToBackStack(name).commit() +} + +fun Context.createDialog( + title: String?, + message: String?, + displayCancel: Boolean = false, + displayOk: Boolean = true, + cancelCallback: DialogInterface.OnClickListener? = null, + okCallback: DialogInterface.OnClickListener? = null, +) { + val builder = AlertDialog.Builder(this) + title?.let { builder.setTitle(it) } + message?.let { builder.setMessage(it) } + if (displayCancel) { + builder.setNegativeButton(android.R.string.cancel, cancelCallback) + } + if (displayOk) { + builder.setPositiveButton(android.R.string.ok, okCallback) + } + + builder.create().show() +} + +fun AppCompatActivity.popBackStack() { + supportFragmentManager.popBackStack() +} + +fun EditText.setTimePicker(onSelected: (String) -> Unit) { + var mHoursOut: Int + var mMinutesOut: Int + + setOnClickListener { + val mCurrentTime by lazy { Calendar.getInstance() } + if (!text.isNullOrEmpty()) { + // EditText contains text - lets try set the parse the text + try { + val convertedString = convertTimeStringToHourMinutesPair(text.toString()) + mHoursOut = convertedString.first + mMinutesOut = convertedString.second + } catch (e: Exception) { + mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY] + mMinutesOut = mCurrentTime[Calendar.MINUTE] + } + } else { + mHoursOut = mCurrentTime[Calendar.HOUR_OF_DAY] + mMinutesOut = mCurrentTime[Calendar.MINUTE] + } + val mTimePicker = TimePickerDialog(this.context, + { _, selectedHour, selectedMinute -> + val ddTime = String.format("%02d", selectedHour) + ":" + String.format( + "%02d", + selectedMinute + ) + setText(ddTime) + onSelected.invoke(ddTime) + }, mHoursOut, mMinutesOut, true + ) //Yes 24 hour time + mTimePicker.setTitle("Select Time") + mTimePicker.show() + } +} + +fun EditText.setDatePicker(onSelected: (String) -> Unit) { + //To show current date in the datepicker + var mYear: Int + var mMonth: Int + var mDay: Int + + val mCurrentDate by lazy { Calendar.getInstance() } + + if (!text.isNullOrEmpty()) { + try { + val dateSplit = text.split("-") + + mYear = dateSplit[0].toInt() + mMonth = dateSplit[1].toInt() + mMonth = if (mMonth == 1) { + 0 + } else { + mMonth - 1 + } + mDay = dateSplit[2].toInt() + } catch (e: Exception) { + mYear = mCurrentDate[Calendar.YEAR] + mMonth = mCurrentDate[Calendar.MONTH] + mDay = mCurrentDate[Calendar.DAY_OF_MONTH] + } + } else { + mYear = mCurrentDate[Calendar.YEAR] + mMonth = mCurrentDate[Calendar.MONTH] + mDay = mCurrentDate[Calendar.DAY_OF_MONTH] + } + val mDatePicker = DatePickerDialog( + (this.context), + { _, selectedYear, selectedMonth, selectedDay -> + var currentMonth = selectedMonth + val dateString = StringBuilder().append(selectedYear).append("-") + .append(String.format("%02d", (currentMonth + 1.also { currentMonth = it }))) + .append("-") + .append(String.format("%02d", selectedDay)) + .toString() + setText(dateString) + onSelected.invoke(dateString) + }, mYear, mMonth, mDay + ) + mDatePicker.setTitle("Select date") + setOnClickListener { + mDatePicker.show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt new file mode 100644 index 0000000..73cc067 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ApplicationViewModelFactory.kt @@ -0,0 +1,25 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.appttude.h_mal.farmr.data.RepositoryImpl + + +class ApplicationViewModelFactory( + private val repository: RepositoryImpl +) : ViewModelProvider.Factory { + + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + with(modelClass) { + return when { + isAssignableFrom(MainViewModel::class.java) -> MainViewModel(repository) + isAssignableFrom(SubmissionViewModel::class.java) -> SubmissionViewModel(repository) + isAssignableFrom(InfoViewModel::class.java) -> InfoViewModel(repository) + isAssignableFrom(FilterViewModel::class.java) -> FilterViewModel(repository) + else -> throw IllegalArgumentException("Unknown ViewModel class") + } as T + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt new file mode 100644 index 0000000..87bb2bd --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/FilterViewModel.kt @@ -0,0 +1,21 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Success + + +class FilterViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun applyFilters( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + super.setFiltrationDetails(description, dateFrom, dateTo, type) + onSuccess(Success("Filter(s) have been applied")) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt new file mode 100644 index 0000000..025829e --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModel.kt @@ -0,0 +1,39 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.os.Bundle +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.ID + + +class InfoViewModel( + repository: Repository +) : ShiftViewModel(repository) { + + fun retrieveData(bundle: Bundle?) { + val id = bundle?.getLong(ID) + if (id == null) { + onError("Failed to retrieve shift") + return + } + + val shift = getCurrentShift(id) + if (shift == null) { + onError("Failed to retrieve shift") + return + } + + onSuccess(shift) + } + + fun buildDurationSummary(shiftObject: ShiftObject): String { + val time = shiftObject.getHoursMinutesPairFromDuration() + + val stringBuilder = StringBuilder().append(time.first).append(" Hours ").append(time.second) + .append(" Minutes ") + if (shiftObject.breakMins > 0) { + stringBuilder.append(" (+ ").append(shiftObject.breakMins).append(" minutes break)") + } + return stringBuilder.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt new file mode 100644 index 0000000..7929615 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/MainViewModel.kt @@ -0,0 +1,289 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.Manifest.permission.WRITE_EXTERNAL_STORAGE +import androidx.annotation.RequiresPermission +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.Observer +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_BREAK +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DESCRIPTION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_DURATION +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_PAYRATE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_IN +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TIME_OUT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TOTALPAY +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_TYPE +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry.COLUMN_SHIFT_UNIT +import com.appttude.h_mal.farmr.data.legacydb.ShiftsContract.ShiftsEntry._ID +import com.appttude.h_mal.farmr.model.Order +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Sortable +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.convertDateString +import com.appttude.h_mal.farmr.utils.formatAsCurrencyString +import com.appttude.h_mal.farmr.utils.sortedByOrder +import jxl.Workbook +import jxl.WorkbookSettings +import jxl.write.Label +import jxl.write.WritableWorkbook +import jxl.write.WriteException +import java.io.File +import java.io.IOException +import java.util.Locale + + +class MainViewModel( + private val repository: Repository +) : ShiftViewModel(repository) { + + private val _shiftLiveData = MutableLiveData>() + private val shiftLiveData: LiveData> = _shiftLiveData + + private var mSort: Sortable = Sortable.ID + private var mOrder: Order = Order.ASCENDING + + private val observer = Observer> { + it?.let { + val result = it.applyFilters().sortList(mSort, mOrder) + onSuccess(result) + } + } + + init { + // Load shifts into live data when view model has been instantiated + refreshLiveData() + shiftLiveData.observeForever(observer) + } + + private fun List.applyFilters(): List { + val filter = getFiltrationDetails() + + return filter { s -> + comparedStrings(filter.type, s.type) && + comparedStringsContains(filter.description, s.description) && + (isBetween(filter.dateFrom, filter.dateTo, s.date) ?: true) + } + } + + private fun comparedStrings(first: String?, second: String?): Boolean { + return when (compareValues(first, second)) { + -1, 0, 1 -> true + else -> { + false + } + } + } + + private fun comparedStringsContains(first: String?, second: String?): Boolean { + first?.let { + (second?.contains(it))?.let { c -> return c } + } + + return comparedStrings(first, second) + } + + private fun isBetween(fromDate: String?, toDate: String?, compareWith: String): Boolean? { + val first = fromDate?.convertDateString() + val second = toDate?.convertDateString() + + if (first == null && second == null) return null + val compareDate = compareWith.convertDateString() ?: return null + + if (second == null) return compareDate.after(first) + if (first == null) return compareDate.before(second) + + return compareDate.after(first) && compareDate.before(second) + } + + + override fun onCleared() { + shiftLiveData.removeObserver(observer) + super.onCleared() + } + + private fun List.sortList(sort: Sortable, order: Order): List { + return when (sort) { + Sortable.ID -> sortedByOrder(order) { it.id } + Sortable.TYPE -> sortedByOrder(order) { it.type } + Sortable.DATE -> sortedByOrder(order) { it.date } + Sortable.DESCRIPTION -> sortedByOrder(order) { it.description } + Sortable.DURATION -> sortedByOrder(order) { it.duration } + Sortable.UNITS -> sortedByOrder(order) { it.units } + Sortable.RATEOFPAY -> sortedByOrder(order) { it.rateOfPay } + Sortable.TOTALPAY -> sortedByOrder(order) { it.totalPay } + } + } + + fun getSortAndOrder(): Pair { + return Pair(mSort, mOrder) + } + + fun setSortAndOrder(sort: Sortable, order: Order = Order.ASCENDING) { + mSort = sort + mOrder = order + refreshLiveData() + } + + fun getInformation(): String { + var totalDuration = 0.0f + var countOfTypeH = 0 + var countOfTypeP = 0 + var totalUnits = 0f + var totalPay = 0f + var lines = 0 + _shiftLiveData.value?.applyFilters()?.forEach { + lines += 1 + totalDuration += it.duration + when (ShiftType.getEnumByType(it.type)) { + ShiftType.HOURLY -> countOfTypeH += 1 + ShiftType.PIECE -> countOfTypeP += 1 + } + totalUnits += it.units + totalPay += it.totalPay + } + + return buildInfoString( + totalDuration, + countOfTypeH, + countOfTypeP, + totalUnits, + totalPay, + lines + ) + } + + fun deleteShift(id: Long) { + if (!repository.deleteSingleShiftFromDatabase(id)) { + onError("Failed to delete shift") + } else { + refreshLiveData() + } + } + + fun deleteAllShifts() { + if (!repository.deleteAllShiftsFromDatabase()) { + onError("Failed to delete all shifts from database") + } else { + refreshLiveData() + } + } + + private fun buildInfoString( + totalDuration: Float, + countOfHourly: Int, + countOfPiece: Int, + totalUnits: Float, + totalPay: Float, + lines: Int + ): String { + val stringBuilder = StringBuilder("$lines Shifts").append("\n") + + if (countOfHourly != 0 && countOfPiece != 0) { + stringBuilder.append(" ($countOfHourly Hourly/$countOfPiece Piece Rate)").append("\n") + } + if (countOfHourly != 0) { + stringBuilder.append("Total Hours: ").append(totalDuration).append("\n") + } + if (countOfPiece != 0) { + stringBuilder.append("Total Units: ").append(totalUnits).append("\n") + } + if (totalPay != 0f) { + stringBuilder.append("Total Pay: ").append(totalPay.formatAsCurrencyString()) + } + return stringBuilder.toString() + } + + fun refreshLiveData() { + repository.readShiftsFromDatabase()?.let { _shiftLiveData.postValue(it) } + } + + fun clearFilters() { + super.setFiltrationDetails(null, null, null, null) + onSuccess(Success("Filters have been cleared")) + refreshLiveData() + } + + @RequiresPermission(WRITE_EXTERNAL_STORAGE) + fun createExcelSheet(file: File): File? { + val wbSettings = WorkbookSettings().apply { + locale = Locale("en", "EN") + } + + try { + val workbook: WritableWorkbook = Workbook.createWorkbook(file, wbSettings) + val sheet = workbook.createSheet("Shifts", 0) + // Write column headers + val headers = listOf( + Label(0, 0, _ID), + Label(1, 0, COLUMN_SHIFT_TYPE), + Label(2, 0, COLUMN_SHIFT_DESCRIPTION), + Label(3, 0, COLUMN_SHIFT_DATE), + Label(4, 0, COLUMN_SHIFT_TIME_IN), + Label(5, 0, COLUMN_SHIFT_TIME_OUT), + Label(6, 0, "$COLUMN_SHIFT_BREAK (in mins)"), + Label(7, 0, COLUMN_SHIFT_DURATION), + Label(8, 0, COLUMN_SHIFT_UNIT), + Label(9, 0, COLUMN_SHIFT_PAYRATE), + Label(10, 0, COLUMN_SHIFT_TOTALPAY) + ) + // table content + if (shiftLiveData.value.isNullOrEmpty()) { + onError("No data to parse into excel file") + return null + } + val sortAndOrder = getSortAndOrder() + val data = shiftLiveData.value!!.applyFilters() + .sortList(sortAndOrder.first, sortAndOrder.second) + var currentRow = 0 + val cells = data.map { shift -> + currentRow += 1 + listOf( + Label(0, currentRow, shift.id.toString()), + Label(1, currentRow, shift.type), + Label(2, currentRow, shift.description), + Label(3, currentRow, shift.date), + Label(4, currentRow, shift.timeIn), + Label(5, currentRow, shift.timeOut), + Label(6, currentRow, shift.breakMins.toString()), + Label(7, currentRow, shift.duration.toString()), + Label(8, currentRow, shift.units.toString()), + Label(9, currentRow, shift.rateOfPay.toString()), + Label(10, currentRow, shift.totalPay.toString()) + ) + }.flatten() + + currentRow += 1 + val footer = listOf( + Label(0, currentRow, "Total:"), + Label(7, currentRow, data.sumOf { it.duration.toDouble() }.toString()), + Label(8, currentRow, data.sumOf { it.units.toDouble() }.toString()), + Label(10, currentRow, data.sumOf { it.totalPay.toDouble() }.toString()) + ) + val content = listOf(headers, cells, footer).flatten() + + // Write content to sheet + try { + content.forEach { c -> sheet.addCell(c) } + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } catch (e: WriteException) { + onError("Failed to write excel sheet") + return null + } + + workbook.write() + workbook.close() + + return file + } catch (e: IOException) { + e.printStackTrace() + onError("Failed to generate excel sheet of shifts") + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt new file mode 100644 index 0000000..abcc710 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModel.kt @@ -0,0 +1,52 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.base.BaseViewModel +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.FilterStore + + +open class ShiftViewModel( + private val repository: Repository +) : BaseViewModel() { + + /* + * Add Item & Further info + */ + fun getCurrentShift(id: Long) = repository.readSingleShiftFromDatabase(id) + + /** + * Lambda function that will invoke onError(...) on failure + * but update live data when successful + */ + private inline fun doTry(operation: () -> Unit) { + try { + operation.invoke() + } catch (e: Exception) { + onError(e) + } + } + + open fun setFiltrationDetails( + description: String?, + dateFrom: String?, + dateTo: String?, + type: String? + ) { + repository.setFilteringDetailsInPrefs(description, dateFrom, dateTo, type) + } + + open fun getFiltrationDetails(): FilterStore { + val prefs = repository.retrieveFilteringDetailsInPrefs() + return FilterStore( + prefs[DESCRIPTION], + prefs[DATE_IN], + prefs[DATE_OUT], + prefs[TYPE] + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt new file mode 100644 index 0000000..322ca40 --- /dev/null +++ b/app/src/main/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModel.kt @@ -0,0 +1,308 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.model.Shift +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.Success +import com.appttude.h_mal.farmr.utils.calculateDuration +import com.appttude.h_mal.farmr.utils.dateStringIsValid +import com.appttude.h_mal.farmr.utils.formatToTwoDp +import com.appttude.h_mal.farmr.utils.getTimeString +import com.appttude.h_mal.farmr.utils.timeStringIsValid +import java.io.IOException +import java.util.Calendar + + +class SubmissionViewModel( + private val repository: Repository +) : ShiftViewModel(repository) { + + fun insertHourlyShift( + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = insertShiftIntoDatabase( + ShiftType.HOURLY, + description, + date, + rateOfPay.formatToTwoDp(), + timeIn, + timeOut, + breakMins, + null + ) + + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun insertPieceRateShift( + description: String, + date: String, + units: Float, + rateOfPay: Float + ) { + // Validate inputs from the edit texts + (description.length > 3).validateField { + onError("Description length should be longer") + return + } + date.dateStringIsValid().validateField { + onError("Date format is invalid") + return + } + (rateOfPay >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + (units.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + + val result = insertShiftIntoDatabase( + type = ShiftType.PIECE, + description = description, + date = date, + rateOfPay = rateOfPay.formatToTwoDp(), + null, + null, + null, + units = units + ) + if (result) onSuccess(Success("New shift successfully added")) + else onError("Cannot insert shift") + } + + fun updateShift( + id: Long, + type: String? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ) { + description?.let { + (it.length > 3).validateField { + onError("Description length should be longer") + return + } + } + date?.dateStringIsValid()?.validateField { + onError("Date format is invalid") + return + } + rateOfPay?.let { + (it >= 0.00).validateField { + onError("Rate of pay is invalid") + return + } + } + units?.let { + (it.toInt() >= 0).validateField { + onError("Units cannot be below zero") + return + } + } + timeIn?.timeStringIsValid()?.validateField { + onError("Time in format is in correct") + return + } + timeOut?.timeStringIsValid()?.validateField { + onError("Time out format is in correct") + return + } + breakMins?.let { it >= 0 }?.validateField { + onError("Break in minutes is invalid") + return + } + + val result = updateShiftInDatabase( + id, + type = type?.let { ShiftType.getEnumByType(it) }, + description = description, + date = date, + rateOfPay = rateOfPay, + timeIn = timeIn, + timeOut = timeOut, + breakMins = breakMins, + units = units + ) + + if (result) onSuccess(Success("Shift successfully updated")) + else onError("Cannot update shift") + } + + private fun updateShiftInDatabase( + id: Long, + type: ShiftType? = null, + description: String? = null, + date: String? = null, + rateOfPay: Float? = null, + timeIn: String? = null, + timeOut: String? = null, + breakMins: Int? = null, + units: Float? = null, + ): Boolean { + val currentShift = repository.readSingleShiftFromDatabase(id)?.copyToShift() + ?: throw IOException("Cannot update shift as it does not exist") + + val shift = when (type) { + ShiftType.HOURLY -> { + // Shift type has changed so mandatory fields for hourly shift are now required as well + val insertTimeIn = + (timeIn ?: currentShift.timeIn) ?: throw IOException("No time in inserted") + val insertTimeOut = + (timeOut ?: currentShift.timeOut) ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + // Shift type has changed so mandatory fields for piece rate shift are now required as well + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + else -> { + if (timeIn == null && timeOut == null && units == null && breakMins == null && rateOfPay == null) { + // Updates to description or date field + currentShift.copy( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + ) + } else { + // Updating shifts where shift type has remained the same + when (currentShift.type) { + ShiftType.HOURLY -> { + val insertTimeIn = (timeIn ?: currentShift.timeIn) ?: throw IOException( + "No time in inserted" + ) + val insertTimeOut = (timeOut ?: currentShift.timeOut) + ?: throw IOException("No time out inserted") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins ?: currentShift.breakMins, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + + ShiftType.PIECE -> { + val insertUnits = (units ?: currentShift.units) + ?: throw IOException("Units must be inserted for piece rate shifts") + Shift( + description = description ?: currentShift.description, + date = date ?: currentShift.date, + units = insertUnits, + rateOfPay = rateOfPay ?: currentShift.rateOfPay + ) + } + } + } + } + } + + return repository.updateShiftIntoDatabase(id, shift) + } + + private fun insertShiftIntoDatabase( + type: ShiftType, + description: String, + date: String, + rateOfPay: Float, + timeIn: String?, + timeOut: String?, + breakMins: Int?, + units: Float?, + ): Boolean { + val shift = when (type) { + ShiftType.HOURLY -> { + if (timeIn.isNullOrBlank() && timeOut.isNullOrBlank()) throw IOException("Time in and time out are null") + val calendar by lazy { Calendar.getInstance() } + val insertTimeIn = timeIn ?: calendar.getTimeString() + val insertTimeOut = timeOut ?: calendar.getTimeString() + Shift( + description = description, + date = date, + timeIn = insertTimeIn, + timeOut = insertTimeOut, + breakMins = breakMins, + rateOfPay = rateOfPay + ) + } + + ShiftType.PIECE -> { + Shift( + description = description, + date = date, + units = units!!, + rateOfPay = rateOfPay, + ) + } + } + + return repository.insertShiftIntoDatabase(shift) + } + + private inline fun Boolean.validateField(failureCallback: () -> Unit) { + if (!this) failureCallback.invoke() + } + + fun retrieveDurationText(mTimeIn: String?, mTimeOut: String?, mBreaks: Int?): Float? { + try { + return calculateDuration(mTimeIn, mTimeOut, mBreaks) + } catch (e: IOException) { + onError(e) + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/farm.jpg b/app/src/main/res/drawable/farm.jpg deleted file mode 100644 index 290da71..0000000 Binary files a/app/src/main/res/drawable/farm.jpg and /dev/null differ diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml deleted file mode 100644 index 34b8202..0000000 --- a/app/src/main/res/drawable/ic_info_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_launcher_release_background.xml b/app/src/main/res/drawable/ic_launcher_release_background.xml deleted file mode 100644 index ca3826a..0000000 --- a/app/src/main/res/drawable/ic_launcher_release_background.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml deleted file mode 100644 index e3400cf..0000000 --- a/app/src/main/res/drawable/ic_notifications_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml deleted file mode 100644 index 2aef437..0000000 --- a/app/src/main/res/drawable/ic_sync_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/img_i_full.png b/app/src/main/res/drawable/img_i_full.png deleted file mode 100644 index 97c195c..0000000 Binary files a/app/src/main/res/drawable/img_i_full.png and /dev/null differ diff --git a/app/src/main/res/layout/empty_list_view.xml b/app/src/main/res/layout/empty_list_view.xml new file mode 100644 index 0000000..8112b34 --- /dev/null +++ b/app/src/main/res/layout/empty_list_view.xml @@ -0,0 +1,38 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_add_item.xml b/app/src/main/res/layout/fragment_add_item.xml index 8f6152b..fd3bf6f 100644 --- a/app/src/main/res/layout/fragment_add_item.xml +++ b/app/src/main/res/layout/fragment_add_item.xml @@ -8,16 +8,9 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.appttude.h_mal.farmr.FragmentAddItem" + tools:context="com.appttude.h_mal.farmr.ui.FragmentAddItem" android:orientation="vertical"> - - diff --git a/app/src/main/res/layout/fragment_filter_data.xml b/app/src/main/res/layout/fragment_filter_data.xml index 3b67fa9..3ebe08f 100644 --- a/app/src/main/res/layout/fragment_filter_data.xml +++ b/app/src/main/res/layout/fragment_filter_data.xml @@ -8,7 +8,7 @@ android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" - tools:context="com.appttude.h_mal.farmr.FilterDataFragment"> + tools:context="com.appttude.h_mal.farmr.ui.FilterDataFragment"> + tools:context="com.appttude.h_mal.farmr.ui.FurtherInfoFragment"> - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_main.xml b/app/src/main/res/layout/fragment_main.xml index 3d50c40..e9fd9c1 100644 --- a/app/src/main/res/layout/fragment_main.xml +++ b/app/src/main/res/layout/fragment_main.xml @@ -2,44 +2,16 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - xmlns:ads="http://schemas.android.com/apk/res-auto" - tools:context="com.appttude.h_mal.farmr.FragmentMain"> + xmlns:app="http://schemas.android.com/apk/res-auto" + tools:context="com.appttude.h_mal.farmr.ui.FragmentMain"> - - - - - - - - - + + app:backgroundTint="@color/colorPrimary" /> + + diff --git a/app/src/main/res/layout/list_item_1.xml b/app/src/main/res/layout/list_item_1.xml index 3ff54df..bdec9ea 100644 --- a/app/src/main/res/layout/list_item_1.xml +++ b/app/src/main/res/layout/list_item_1.xml @@ -126,7 +126,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentEnd="true" - android:layout_alignParentTop="true" app:srcCompat="@android:drawable/ic_menu_edit" /> diff --git a/app/src/main/res/layout/main_view.xml b/app/src/main/res/layout/main_view.xml index 790957a..5dcff32 100644 --- a/app/src/main/res/layout/main_view.xml +++ b/app/src/main/res/layout/main_view.xml @@ -2,7 +2,7 @@ diff --git a/app/src/main/res/menu/menu_main.xml b/app/src/main/res/menu/menu_main.xml index 0135299..e4980fb 100644 --- a/app/src/main/res/menu/menu_main.xml +++ b/app/src/main/res/menu/menu_main.xml @@ -1,7 +1,7 @@ + tools:context="com.appttude.h_mal.farmr.ui.MainActivity"> Farmr - Settings Shifts Add Shift Edit Shift - Delete Shift failed to insert Shift Shift successfully added Update Failed @@ -25,11 +23,6 @@ General - Enable social recommendations - Recommendations for people to contact - based on your message history - - Display name John Smith @@ -102,4 +95,6 @@ Hello blank fragment Shift Details + insert break in minutes + Break diff --git a/app/src/main/res/xml/provider_path.xml b/app/src/main/res/xml/provider_path.xml new file mode 100644 index 0000000..4495c28 --- /dev/null +++ b/app/src/main/res/xml/provider_path.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java b/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java deleted file mode 100644 index cfb3bfd..0000000 --- a/app/src/test/java/com/appttude/h_mal/farmr/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.appttude.h_mal.farmr; - -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt new file mode 100644 index 0000000..4688aa6 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/data/RepositoryImplTest.kt @@ -0,0 +1,51 @@ +package com.appttude.h_mal.farmr.data + +import com.appttude.h_mal.farmr.data.legacydb.LegacyDatabase +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.PreferenceProvider +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockk +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.anyLong +import kotlin.test.assertEquals +import kotlin.test.assertIs + +class RepositoryImplTest { + + private lateinit var repository: RepositoryImpl + + @MockK + lateinit var db: LegacyDatabase + + @MockK + lateinit var prefs: PreferenceProvider + + @Before + fun setUp() { + MockKAnnotations.init(this) + repository = RepositoryImpl(db, prefs) + } + + @Test + fun readDatabase_validResponse() { + // Arrange + val elements = listOf( + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() }, + mockk { every { id } returns anyLong() } + ) + + //Act + every { db.readShiftsFromDatabase() } returns elements + + // Assert + val result = repository.readShiftsFromDatabase() + assertIs>(result) + assertEquals(result.first().id, anyLong()) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt new file mode 100644 index 0000000..e04e3ce --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/utils/testUtils.kt @@ -0,0 +1,149 @@ +package com.appttude.h_mal.farmr.utils + +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import org.mockito.ArgumentMatchers +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun LiveData.getOrAwaitValue( + time: Long = 2, + timeUnit: TimeUnit = TimeUnit.SECONDS +): T { + var data: T? = null + val latch = CountDownLatch(1) + val observer = object : Observer { + override fun onChanged(o: T?) { + data = o + latch.countDown() + this@getOrAwaitValue.removeObserver(this) + } + } + + this.observeForever(observer) + + // Don't wait indefinitely if the LiveData is not set. + if (!latch.await(time, timeUnit)) { + throw TimeoutException("LiveData value was never set.") + } + + @Suppress("UNCHECKED_CAST") + return data as T +} + +fun sleep(millis: Long = 1000) { + runBlocking(Dispatchers.Default) { delay(millis) } +} + +fun getShifts() = listOf( + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), +) \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt new file mode 100644 index 0000000..3fb73c0 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/InfoViewModelTest.kt @@ -0,0 +1,93 @@ +package com.appttude.h_mal.farmr.viewmodel + +import android.os.Bundle +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.utils.ID +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Test +import org.mockito.ArgumentMatchers.anyLong +import kotlin.test.assertIs + +class InfoViewModelTest : ShiftViewModelTest() { + + @Test + fun retrieveData_validBundleAndId_successfulRetrieval() { + // Arrange + val id = anyLong() + val shift = mockk() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(shift) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(bundle) + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + retrieveCurrentData(), + shift + ) + } + + @Test + fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() { + // Arrange + val id = anyLong() + val shift = mockk() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(shift) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(null) + + // Assert + assertEquals( + retrieveCurrentError(), + "Failed to retrieve shift" + ) + } + + @Test + fun retrieveData_validBundleNoShift_successfulRetrieval() { + // Arrange + val id = anyLong() + val bundle = mockk() + + // Act + every { repository.readSingleShiftFromDatabase(id) }.returns(null) + every { bundle.getLong(ID) }.returns(id) + viewModel.retrieveData(bundle) + + // Assert + assertEquals( + retrieveCurrentError(), + "Failed to retrieve shift" + ) + } + + @Test + fun buildDurationSummary_validHourlyShift_successfulRetrieval() { + // Arrange + val shift = getShifts()[0] + val shiftWithBreak = getShifts()[3] + + // Act + val summary = viewModel.buildDurationSummary(shift) + val summaryWithBreak = viewModel.buildDurationSummary(shiftWithBreak) + + // Assert + assertEquals( + "1 Hours 0 Minutes ", + summary + ) + assertEquals( + "1 Hours 0 Minutes (+ 30 minutes break)", + summaryWithBreak + ) + } + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt new file mode 100644 index 0000000..4ba2d24 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/MainViewModelTest.kt @@ -0,0 +1,130 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.data.prefs.DATE_IN +import com.appttude.h_mal.farmr.data.prefs.DATE_OUT +import com.appttude.h_mal.farmr.data.prefs.DESCRIPTION +import com.appttude.h_mal.farmr.data.prefs.TYPE +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import com.appttude.h_mal.farmr.utils.getShifts +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyList +import java.util.concurrent.TimeoutException +import kotlin.test.assertEquals + +class MainViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + private lateinit var repository: Repository + private lateinit var viewModel: MainViewModel + + @Before + fun setUp() { + repository = mockk() + every { repository.readShiftsFromDatabase() }.returns(null) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel = MainViewModel(repository) + } + + @Test + fun initViewModel_liveDataIsEmpty() { + // Assert + assertThrows(TimeoutException::class.java) { viewModel.uiState.getOrAwaitValue() } + } + + @Test + fun getShiftsFromRepository_liveDataIsShown() { + // Arrange + val listOfShifts = anyList() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + + // Assert + assertEquals(retrieveCurrentData(), listOfShifts) + } + + @Test + fun getShiftsFromRepository_liveDataIsShown_defaultFiltersAndSortsValid() { + // Arrange + val listOfShifts = getShifts() + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, listOfShifts) + assertEquals( + description, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + @Test + fun getShiftsFromRepository_applyFiltersThenClearFilters_descriptionIsValid() { + // Arrange + val listOfShifts = getShifts() + val filteredShifts = getShifts().filter { it.type == ShiftType.HOURLY.type } + + // Act + every { repository.readShiftsFromDatabase() }.returns(listOfShifts) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter(type = ShiftType.HOURLY.type)) + viewModel.refreshLiveData() + val retrievedShifts = retrieveCurrentData() + val description = viewModel.getInformation() + + every { repository.setFilteringDetailsInPrefs(null, null, null, null) }.returns(Unit) + every { repository.retrieveFilteringDetailsInPrefs() }.returns(getFilter()) + viewModel.clearFilters() + val descriptionAfterClearedFilter = viewModel.getInformation() + + // Assert + assertEquals(retrievedShifts, filteredShifts) + assertEquals( + description, "4 Shifts\n" + + "Total Hours: 4.0\n" + + "Total Pay: £30.00" + ) + assertEquals( + descriptionAfterClearedFilter, "8 Shifts\n" + + " (4 Hourly/4 Piece Rate)\n" + + "Total Hours: 4.0\n" + + "Total Units: 4.0\n" + + "Total Pay: £70.00" + ) + } + + private fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + private fun getFilter( + description: String? = null, + type: String? = null, + dateIn: String? = null, + dateOut: String? = null + ): Map = + mapOf( + Pair(DESCRIPTION, description), + Pair(DATE_IN, dateIn), + Pair(DATE_OUT, dateOut), + Pair(TYPE, type) + ) + +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt new file mode 100644 index 0000000..1106249 --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/ShiftViewModelTest.kt @@ -0,0 +1,171 @@ +package com.appttude.h_mal.farmr.viewmodel + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import com.appttude.h_mal.farmr.data.Repository +import com.appttude.h_mal.farmr.data.legacydb.ShiftObject +import com.appttude.h_mal.farmr.model.ShiftType +import com.appttude.h_mal.farmr.model.ViewState +import com.appttude.h_mal.farmr.utils.getOrAwaitValue +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.RelaxedMockK +import org.junit.Before +import org.junit.Rule +import org.mockito.ArgumentMatchers + +open class ShiftViewModelTest { + @get:Rule + val rule = InstantTaskExecutorRule() + + @RelaxedMockK + lateinit var repository: Repository + + @InjectMockKs + lateinit var viewModel: V + + @Before + fun setUp() { + MockKAnnotations.init(this) + } + + fun retrieveCurrentData() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasData<*>).data + + fun retrieveCurrentError() = + (viewModel.uiState.getOrAwaitValue() as ViewState.HasError<*>).error + + fun getHourlyShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ) + + fun getPieceRateShift() = ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ) + + fun getShifts() = listOf( + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day one", + "2023-08-01", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day two", + "2023-08-02", + "12:00", + "13:00", + 1f, + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyFloat(), + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day three", + "2023-08-03", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.HOURLY.type, + "Day four", + "2023-08-04", + "12:00", + "13:00", + 1f, + 30, + ArgumentMatchers.anyFloat(), + 10f, + 5f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day five", + "2023-08-05", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day six", + "2023-08-06", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day seven", + "2023-08-07", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ShiftObject( + ArgumentMatchers.anyLong(), + ShiftType.PIECE.type, + "Day eight", + "2023-08-08", + ArgumentMatchers.anyString(), + ArgumentMatchers.anyString(), + ArgumentMatchers.anyFloat(), + ArgumentMatchers.anyInt(), + 1f, + 10f, + 10f + ), + ) +} \ No newline at end of file diff --git a/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt new file mode 100644 index 0000000..2f6244b --- /dev/null +++ b/app/src/test/java/com/appttude/h_mal/farmr/viewmodel/SubmissionViewModelTest.kt @@ -0,0 +1,85 @@ +package com.appttude.h_mal.farmr.viewmodel + +import com.appttude.h_mal.farmr.model.Success +import io.mockk.every +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertIs + +class SubmissionViewModelTest : ShiftViewModelTest() { + + @Test + fun insertHourlyShifts_validParameters_successfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(true) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertPieceShifts_validParameters_successfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(true) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertIs(retrieveCurrentData()) + assertEquals( + (retrieveCurrentData() as Success).successMessage, + "New shift successfully added" + ) + } + + @Test + fun insertHourlyShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val hourly = getHourlyShift() + + // Act + every { repository.insertShiftIntoDatabase(hourly.copyToShift()) }.returns(false) + hourly.run { + viewModel.insertHourlyShift(description, date, rateOfPay, timeIn, timeOut, breakMins) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + + @Test + fun insertPieceShifts_validParameters_unsuccessfulInsertions() { + // Arrange + val piece = getPieceRateShift() + + // Act + every { repository.insertShiftIntoDatabase(piece.copyToShift()) }.returns(false) + piece.run { + viewModel.insertPieceRateShift(description, date, units, rateOfPay) + } + + // Assert + assertEquals( + retrieveCurrentError(), + "Cannot insert shift" + ) + } + +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index f56abf1..4e3983e 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ buildscript { ext { - kotlin_version = '1.9.0' + kotlin_version = '1.7.10' } repositories { google() diff --git a/fastlane/Appfile b/fastlane/Appfile new file mode 100644 index 0000000..116ee2e --- /dev/null +++ b/fastlane/Appfile @@ -0,0 +1,2 @@ +json_key_file("google-play-key.json") # Path to the json secret file - Follow https://docs.fastlane.tools/actions/supply/#setup to get one +package_name("com.appttude.h_mal.farmr") # e.g. com.krausefx.app diff --git a/fastlane/Fastfile b/fastlane/Fastfile new file mode 100644 index 0000000..5d272f7 --- /dev/null +++ b/fastlane/Fastfile @@ -0,0 +1,29 @@ +# This file contains the fastlane.tools configuration +# You can find the documentation at https://docs.fastlane.tools +# +# For a list of all available actions, check out +# +# https://docs.fastlane.tools/actions +# +# For a list of all available plugins, check out +# +# https://docs.fastlane.tools/plugins/available-plugins +# + +# Uncomment the line if you want fastlane to automatically update itself +# update_fastlane + +default_platform(:android) + +platform :android do + desc "Runs all the tests" + lane :test do + gradle(task: "test") + end + + desc "Deploy a new version to the Google Play" + lane :deploy do + gradle(task: "clean assembleRelease") + upload_to_play_store + end +end diff --git a/fastlane/README.md b/fastlane/README.md new file mode 100644 index 0000000..5649d3a --- /dev/null +++ b/fastlane/README.md @@ -0,0 +1,40 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## Android + +### android test + +```sh +[bundle exec] fastlane android test +``` + +Runs all the tests + +### android deploy + +```sh +[bundle exec] fastlane android deploy +``` + +Deploy a new version to the Google Play + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).