Merge pull request #19 from hmalik144/modern_architecture

Modern architecture
MVVM
clean architecture
test suite expansion
CI/CD
fastlane implementation
This commit is contained in:
2023-08-29 21:21:31 +01:00
committed by GitHub
107 changed files with 4945 additions and 2285 deletions

View File

@@ -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
workflows:
say-hello-workflow:
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:
- say-hello
# 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:
version: 2
build-release:
jobs:
- 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

102
.gitignore vendored
View File

@@ -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

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AndroidTestResultsUserPreferences">
<option name="androidTestResultsTableState">
<map>
<entry key="1283002349">
<value>
<AndroidTestResultsTableState>
<option name="preferredColumnWidths">
<map>
<entry key="Duration" value="90" />
<entry key="Pixel_6_Pro_API_31" value="120" />
<entry key="Tests" value="360" />
</map>
</option>
</AndroidTestResultsTableState>
</value>
</entry>
</map>
</option>
</component>
</project>

View File

@@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="WizardSettings">
<option name="children">
<map>
<entry key="imageWizard">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="imageAssetPanel">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="actionbar">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipArt">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="color" value="000000" />
<entry key="opacityPercent" value="80" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="theme" value="HOLO_DARK" />
<entry key="themeColor" value="ffffff" />
</map>
</option>
</PersistentState>
</value>
</entry>
<entry key="launcher">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="foregroundImage">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="color" value="000000" />
<entry key="scalingPercent" value="69" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
<option name="values">
<map>
<entry key="foregroundImage" value="C:\Users\h_mal\Desktop\Farmr\farmicon.png" />
<entry key="outputName" value="ic_launcher_release" />
</map>
</option>
</PersistentState>
</value>
</entry>
<entry key="launcherLegacy">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipArt">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="color" value="000000" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
<entry key="notification">
<value>
<PersistentState>
<option name="children">
<map>
<entry key="clipArt">
<value>
<PersistentState>
<option name="values">
<map>
<entry key="color" value="000000" />
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</PersistentState>
</value>
</entry>
</map>
</option>
</component>
</project>

Binary file not shown.

Binary file not shown.

View File

@@ -1,113 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<code_scheme name="Project" version="173">
<codeStyleSettings language="XML">
<arrangement>
<rules>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:android</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>xmlns:.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:id</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*:name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>name</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>style</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>^$</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
</AND>
</match>
<order>ANDROID_ATTRIBUTE_ORDER</order>
</rule>
</section>
<section>
<rule>
<match>
<AND>
<NAME>.*</NAME>
<XML_ATTRIBUTE />
<XML_NAMESPACE>.*</XML_NAMESPACE>
</AND>
</match>
<order>BY_NAME</order>
</rule>
</section>
</rules>
</arrangement>
</codeStyleSettings>
</code_scheme>
</component>

6
.idea/compiler.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="17" />
</component>
</project>

21
.idea/gradle.xml generated
View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="false" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="gradleJvm" value="jbr-17" />
<option name="modules">
<set>
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
</set>
</option>
</GradleProjectSettings>
</option>
</component>
</project>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="BintrayJCenter" />
<option name="name" value="BintrayJCenter" />
<option name="url" value="https://jcenter.bintray.com/" />
</remote-repository>
<remote-repository>
<option name="id" value="maven" />
<option name="name" value="maven" />
<option name="url" value="https://jitpack.io" />
</remote-repository>
<remote-repository>
<option name="id" value="Google" />
<option name="name" value="Google" />
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
</remote-repository>
</component>
</project>

2
.idea/kotlinc.xml generated
View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="1.9.0" />
<option name="version" value="1.7.10" />
</component>
</project>

47
.idea/misc.xml generated
View File

@@ -1,47 +0,0 @@
<project version="4">
<component name="NullableNotNullManager">
<option name="myDefaultNullable" value="android.support.annotation.Nullable" />
<option name="myDefaultNotNull" value="android.support.annotation.NonNull" />
<option name="myNullables">
<value>
<list size="12">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.Nullable" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nullable" />
<item index="2" class="java.lang.String" itemvalue="javax.annotation.CheckForNull" />
<item index="3" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.Nullable" />
<item index="4" class="java.lang.String" itemvalue="android.support.annotation.Nullable" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.Nullable" />
<item index="6" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNullable" />
<item index="7" class="java.lang.String" itemvalue="android.annotation.Nullable" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.Nullable" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableDecl" />
<item index="10" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NullableType" />
<item index="11" class="java.lang.String" itemvalue="com.android.annotations.Nullable" />
</list>
</value>
</option>
<option name="myNotNulls">
<value>
<list size="11">
<item index="0" class="java.lang.String" itemvalue="org.jetbrains.annotations.NotNull" />
<item index="1" class="java.lang.String" itemvalue="javax.annotation.Nonnull" />
<item index="2" class="java.lang.String" itemvalue="edu.umd.cs.findbugs.annotations.NonNull" />
<item index="3" class="java.lang.String" itemvalue="android.support.annotation.NonNull" />
<item index="4" class="java.lang.String" itemvalue="androidx.annotation.NonNull" />
<item index="5" class="java.lang.String" itemvalue="androidx.annotation.RecentlyNonNull" />
<item index="6" class="java.lang.String" itemvalue="android.annotation.NonNull" />
<item index="7" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.qual.NonNull" />
<item index="8" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullDecl" />
<item index="9" class="java.lang.String" itemvalue="org.checkerframework.checker.nullness.compatqual.NonNullType" />
<item index="10" class="java.lang.String" itemvalue="com.android.annotations.NonNull" />
</list>
</value>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">
<option name="id" value="Android" />
</component>
</project>

12
.idea/modules.xml generated
View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/modules/Farmr.iml" filepath="$PROJECT_DIR$/.idea/modules/Farmr.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.androidTest.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.main.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" filepath="$PROJECT_DIR$/.idea/modules/app/Farmr.app.unitTest.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated
View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

3
Gemfile Normal file
View File

@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"

View File

@@ -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'
}

View File

@@ -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<Shift>) = shifts.forEach { addToDatabase(it) }
fun clearDatabase() = database.deleteAllShiftsInDatabase()
fun cleanPrefs() = preferenceProvider.clearPrefs()
}

View File

@@ -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)
}
}

View File

@@ -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))

View File

@@ -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)
}
}

View File

@@ -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<A : Activity>(
private val activity: Class<A>,
private val intentBundle: Bundle? = null,
) {
lateinit var scenario: ActivityScenario<A>
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<View> = 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()
}

View File

@@ -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 <VH : ViewHolder> scrollToRecyclerItem(recyclerId: Int, text: String): ViewInteraction? {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>(
hasDescendant(withText(text))
)
)
}
fun <VH : ViewHolder> scrollToRecyclerItem(
recyclerId: Int,
resIdForString: Int
): ViewInteraction? {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>(
hasDescendant(withText(resIdForString))
)
)
}
fun <VH : ViewHolder> scrollToRecyclerItemByPosition(
recyclerId: Int,
position: Int
): ViewInteraction? {
return matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollToPosition<VH>(position)
)
}
fun <VH : ViewHolder> clickViewInRecycler(recyclerId: Int, resIdForString: Int) {
matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.actionOnItem<VH>(
hasDescendant(withText(resIdForString)),
click()
)
)
}
fun <VH : ViewHolder> clickRecyclerAtPosition(recyclerId: Int, position: Int) {
matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollToPosition<VH>(position),
RecyclerViewActions.actionOnItemAtPosition<VH>(position, click()),
)
}
fun <VH : ViewHolder> clickViewInRecyclerAtPosition(recyclerId: Int, position: Int, subViewId: Int) {
matchView(recyclerId)
.perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollToPosition<VH>(position),
RecyclerViewActions.actionOnItemAtPosition<VH>(position, object : ViewAction {
override fun getDescription(): String {
return "click on subview in RecyclerView at position: $position"
}
override fun getConstraints(): Matcher<View> {
return Matchers.allOf(
isAssignableFrom(
RecyclerView::class.java
), isDisplayed()
)
}
override fun perform(uiController: UiController?, view: View?) {
view?.findViewById<View>(subViewId)?.performClick()
}
}),
)
}
fun <VH : ViewHolder> clickOnRecyclerItemWithText(recyclerId: Int, text: String) {
matchView(recyclerId).perform(
// scrollTo will fail the test if no item matches.
RecyclerViewActions.scrollTo<VH>(
hasDescendant(withText(text))
),
RecyclerViewActions.actionOnItem<VH>(
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()
)
}
}

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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<CurrentViewHolder>(R.id.list_item_view, text)
fun clickOnItemAtPosition(position: Int) = clickRecyclerAtPosition<CurrentViewHolder>(R.id.list_item_view, position)
fun clickOnEdit(position: Int) = clickViewInRecyclerAtPosition<CurrentViewHolder>(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)
}
}

View File

@@ -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)
}

View File

@@ -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>(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() {
}
}

View File

@@ -0,0 +1 @@
package com.appttude.h_mal.farmr.ui.utils

View File

@@ -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
)
)

View File

@@ -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<View>): ViewAction {
return object : ViewAction {
override fun getConstraints(): Matcher<View> = 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<View> = 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<View> {
return object : BaseMatcher<View>() {
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<View>,
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")
}
}

View File

@@ -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 <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
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
}

View File

@@ -7,6 +7,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:name=".di.ShiftApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -16,7 +17,7 @@
<!-- Splash screen -->
<activity
android:name="com.appttude.h_mal.farmr.SplashScreen"
android:name="com.appttude.h_mal.farmr.ui.SplashScreen"
android:screenOrientation="portrait"
android:theme="@android:style/Theme.Black.NoTitleBar"
tools:ignore="LockedOrientationActivity"
@@ -27,15 +28,25 @@
</intent-filter>
</activity>
<activity
android:name="com.appttude.h_mal.farmr.MainActivity"
android:parentActivityName="com.appttude.h_mal.farmr.SplashScreen"
android:name="com.appttude.h_mal.farmr.ui.MainActivity"
android:parentActivityName="com.appttude.h_mal.farmr.ui.SplashScreen"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true"/>
<provider
android:name="com.appttude.h_mal.farmr.data.ShiftProvider"
android:name="com.appttude.h_mal.farmr.data.legacydb.ShiftProvider"
android:authorities="com.appttude.h_mal.farmr"
android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_path" />
</provider>
</application>
</manifest>

View File

@@ -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<String> = arrayOf("", "Hourly", "Piece Rate")
private val listArgs: MutableList<String> = 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<View>(R.id.filterLocationEditText) as EditText?
dateFromET = rootView.findViewById<View>(R.id.fromdateInEditText) as EditText?
dateToET = rootView.findViewById<View>(R.id.filterDateOutEditText) as EditText?
typeSpinner = rootView.findViewById<View>(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<String> = 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<View>(R.id.submitFiltered) as Button
submit.setOnClickListener {
BuildQuery()
activity.args = listArgs.toTypedArray<String>()
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())
}
}
}

View File

@@ -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<Cursor?> {
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<View>(R.id.pd_ai) as ProgressBar
scrollView = rootView.findViewById<View>(R.id.total_view) as ScrollView
mRadioGroup = rootView.findViewById<View>(R.id.rg) as RadioGroup
mRadioButtonOne = rootView.findViewById<View>(R.id.hourly) as RadioButton
mRadioButtonTwo = rootView.findViewById<View>(R.id.piecerate) as RadioButton
mLocationEditText = rootView.findViewById<View>(R.id.locationEditText) as EditText
mDateEditText = rootView.findViewById<View>(R.id.dateEditText) as EditText
mTimeInEditText = rootView.findViewById<View>(R.id.timeInEditText) as EditText
mBreakEditText = rootView.findViewById<View>(R.id.breakEditText) as EditText
mTimeOutEditText = rootView.findViewById<View>(R.id.timeOutEditText) as EditText
mDurationTextView = rootView.findViewById<View>(R.id.ShiftDuration) as TextView
mUnitEditText = rootView.findViewById<View>(R.id.unitET) as EditText
mPayRateEditText = rootView.findViewById<View>(R.id.payrateET) as EditText
mTotalPayTextView = rootView.findViewById<View>(R.id.totalpayval) as TextView
hourlyDataView = rootView.findViewById<View>(R.id.hourly_data_holder) as LinearLayout
unitsHolder = rootView.findViewById<View>(R.id.units_holder) as LinearLayout
durationHolder = rootView.findViewById<View>(R.id.duration_holder) as LinearLayout
wholeView = rootView.findViewById<View>(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<View>(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<Cursor?> {
progressBarAI!!.visibility = View.VISIBLE
scrollView!!.visibility = View.GONE
val projection = arrayOf<String?>(
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<Cursor?>) {}
override fun onLoadFinished(loader: Loader<Cursor?>, 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
}
}

View File

@@ -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<Cursor> {
var mCursorAdapter: ShiftsCursorAdapter? = null
var shiftsDbhelper: ShiftsDbHelper? = null
lateinit var defaultLoaderCallback: LoaderManager.LoaderCallbacks<Cursor>
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<View>(R.id.list_item_view) as ListView
val emptyView = rootView.findViewById<View>(R.id.empty_view)
productListView.emptyView = emptyView
mCursorAdapter = ShiftsCursorAdapter(activity, null)
productListView.adapter = mCursorAdapter
loaderManager.initLoader(DEFAULT_LOADER, null, defaultLoaderCallback)
val fab = rootView.findViewById<FloatingActionButton>(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<String?>("")
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<String?>(
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<String?>(
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<Cursor> {
val projection = arrayOf<String?>(
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: Cursor) {
mCursorAdapter!!.swapCursor(cursor)
}
override fun onLoaderReset(loader: Loader<Cursor>) {
mCursorAdapter!!.swapCursor(null)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, 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
}
}
}

View File

@@ -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<Cursor> {
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<View>(R.id.progressBar_info) as ProgressBar?
wholeView = rootView.findViewById<View>(R.id.further_info_view) as LinearLayout?
typeTV = rootView.findViewById<View>(R.id.details_shift) as TextView?
descriptionTV = rootView.findViewById<View>(R.id.details_desc) as TextView?
dateTV = rootView.findViewById<View>(R.id.details_date) as TextView?
times = rootView.findViewById<View>(R.id.details_time) as TextView?
breakTV = rootView.findViewById<View>(R.id.details_breaks) as TextView?
durationTV = rootView.findViewById<View>(R.id.details_duration) as TextView?
unitsTV = rootView.findViewById<View>(R.id.details_units) as TextView?
payRateTV = rootView.findViewById<View>(R.id.details_pay_rate) as TextView?
totalPayTV = rootView.findViewById<View>(R.id.details_totalpay) as TextView?
editButton = rootView.findViewById<View>(R.id.details_edit) as Button?
hourlyDetailHolder = rootView.findViewById<View>(R.id.details_hourly_details) as LinearLayout?
unitsHolder = rootView.findViewById<View>(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<Cursor?> {
progressBarFI!!.visibility = View.VISIBLE
wholeView!!.visibility = View.GONE
val projection: Array<String?> = 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: 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<Cursor>) {}
companion object {
private val DEFAULT_LOADER: Int = 0
}
}

View File

@@ -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<String>? = 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<View>(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
)
}
}
}

View File

@@ -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<View>(R.id.location) as TextView
val dateTextView: TextView = view.findViewById<View>(R.id.date) as TextView
val totalPay: TextView = view.findViewById<View>(R.id.total_pay) as TextView
val hoursView: TextView = view.findViewById<View>(R.id.hours) as TextView
val h: TextView = view.findViewById<View>(R.id.h) as TextView
val minutesView: TextView = view.findViewById<View>(R.id.minutes) as TextView
val m: TextView = view.findViewById<View>(R.id.m) as TextView
val editView: ImageView = view.findViewById<View>(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<String> = 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<String> {
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)
}
}
}

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.farmr.base
interface BackPressedListener {
fun onBackPressed(): Boolean
}

View File

@@ -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 <A : AppCompatActivity> startActivity(activity: Class<A>) {
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
}
}

View File

@@ -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
}

View File

@@ -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<V : BaseViewModel>(@LayoutRes contentLayoutId: Int) :
Fragment(contentLayoutId), KodeinAware {
override val kodein by kodein()
private val factory by instance<ApplicationViewModelFactory>()
val viewModel: V by getViewModel()
private fun getViewModel(): Lazy<V> =
ViewModelLazy(getGenericClassAt(0), storeProducer = { viewModelStore },
factoryProducer = { factory } )
var mActivity: BaseActivity? = null
private var shortAnimationDuration by Delegates.notNull<Int>()
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()
}

View File

@@ -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<T: Any>(
@LayoutRes private val emptyViewId: Int,
@LayoutRes private val currentViewId: Int
): RecyclerView.Adapter<ViewHolder>() {
var list: List<T>? = 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)
}

View File

@@ -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<ViewState>()
val uiState: LiveData<ViewState> = _uiState
fun onStart() {
_uiState.postValue(ViewState.HasStarted)
}
fun <T : Any> onSuccess(result: T) {
_uiState.postValue(ViewState.HasData(result))
}
protected fun <E : Any> onError(error: E) {
_uiState.postValue(ViewState.HasError(error))
}
}

View File

@@ -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<ShiftObject>?
fun readSingleShiftFromDatabase(id: Long): ShiftObject?
fun deleteSingleShiftFromDatabase(id: Long): Boolean
fun deleteAllShiftsFromDatabase(): Boolean
fun retrieveSortAndOrderFromPref(): Pair<Sortable?, Order?>
fun setSortAndOrderFromPref(sortable: Sortable, order: Order)
fun retrieveFilteringDetailsInPrefs(): Map<String, String?>
fun setFilteringDetailsInPrefs(
description: String?,
timeIn: String?,
timeOut: String?,
type: String?
)
}

View File

@@ -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<ShiftObject>? {
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<Sortable?, Order?> {
return preferenceProvider.getSortableAndOrder()
}
override fun setSortAndOrderFromPref(sortable: Sortable, order: Order) {
preferenceProvider.saveSortableAndOrder(sortable, order)
}
override fun retrieveFilteringDetailsInPrefs(): Map<String, String?> {
return preferenceProvider.getFilteringDetails()
}
override fun setFilteringDetailsInPrefs(
description: String?,
timeIn: String?,
timeOut: String?,
type: String?
) {
preferenceProvider.saveFilteringDetails(description, timeIn, timeOut, type)
}
}

View File

@@ -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<String?>(
_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<ShiftObject>? {
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<String> = 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
)
}
}

View File

@@ -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<String, String> {
val hours: Int = floor(duration).toInt()
val minutes: Int = ((duration - hours) * 60).toInt()
return Pair(hours.toString(), minutes.toString())
}
}

View File

@@ -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<String>?, selection: String?, selectionArgs: Array<String>?,
sortOrder: String?): Cursor? {
var selection = selection
var selectionArgs = selectionArgs
override fun query(
uri: Uri, projection: Array<String>?, selection: String?, selectionArgs: Array<String>?,
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,25 +46,24 @@ 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)
values!!.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
?: throw IllegalArgumentException("Description required")
val date = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
values.getAsString(ShiftsEntry.COLUMN_SHIFT_DATE)
?: throw IllegalArgumentException("Date required")
val timeIn = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_IN)
?: throw IllegalArgumentException("Time In required")
val timeOut = values.getAsString(ShiftsEntry.COLUMN_SHIFT_TIME_OUT)
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)
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" }
@@ -84,42 +83,46 @@ class ShiftProvider : ContentProvider() {
return ContentUris.withAppendedId(uri, id)
}
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?): 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<String>?
): 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<String>?): Int {
private fun updateShift(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<String>?
): Int {
if (values!!.containsKey(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)) {
val description = values.getAsString(ShiftsEntry.COLUMN_SHIFT_DESCRIPTION)
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)
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)
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)
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)
values.getAsString(ShiftsEntry.COLUMN_SHIFT_BREAK)
?: throw IllegalArgumentException("break required")
}
if (values.size() == 0) {
@@ -134,17 +137,15 @@ class ShiftProvider : ContentProvider() {
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): 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
)
}
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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<Sortable?, Order?> {
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<String, String?> {
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()
}
}

View File

@@ -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)
}

View File

@@ -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
)

View File

@@ -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?
)

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.farmr.model
enum class Order(val label: String) {
ASCENDING("Ascending"), DESCENDING("Descending")
}

View File

@@ -1,5 +1,8 @@
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,
@@ -11,4 +14,52 @@ data class Shift(
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()
)
}
}

View File

@@ -2,5 +2,11 @@ package com.appttude.h_mal.farmr.model
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 }
}
}
}

View File

@@ -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 }
}
}
}

View File

@@ -0,0 +1,5 @@
package com.appttude.h_mal.farmr.model
data class Success(
val successMessage: String
)

View File

@@ -0,0 +1,7 @@
package com.appttude.h_mal.farmr.model
sealed class ViewState {
object HasStarted : ViewState()
class HasData<T : Any>(val data: T) : ViewState()
class HasError<T : Any>(val error: T) : ViewState()
}

View File

@@ -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<FilterViewModel>(R.layout.fragment_filter_data),
AdapterView.OnItemSelectedListener, OnClickListener {
private val spinnerList: Array<String> =
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<String> =
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()
}
}

View File

@@ -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<SubmissionViewModel>(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()
}
}
}

View File

@@ -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<MainViewModel>(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<FloatingActionButton>(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<ShiftObject>)
}
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<String>,
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
}
}

View File

@@ -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<InfoViewModel>(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
}
}
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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<ShiftObject, BaseRecyclerAdapter.CurrentViewHolder>(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<ShiftObject>() {
override fun areItemsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: ShiftObject, newItem: ShiftObject): Boolean {
return oldItem == newItem
}
}
}
}

View File

@@ -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<View>(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
}
}

View File

@@ -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 = "£"

View File

@@ -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<Int, Int> {
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)
}

View File

@@ -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 <CLASS : Any> Any.getGenericClassAt(position: Int): KClass<CLASS> =
((javaClass.genericSuperclass as? ParameterizedType)
?.actualTypeArguments?.getOrNull(position) as? Class<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: Any?> 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 <T, R : Comparable<R>> Iterable<T>.sortedByOrder(order: Order = Order.ASCENDING, crossinline selector: (T) -> R?): List<T> {
return when (order) {
Order.ASCENDING -> sortedWith(compareBy(selector))
Order.DESCENDING -> sortedWith(compareByDescending(selector))
}
}

View File

@@ -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()
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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
}
}
}

View File

@@ -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"))
}
}

View File

@@ -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()
}
}

View File

@@ -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<List<ShiftObject>>()
private val shiftLiveData: LiveData<List<ShiftObject>> = _shiftLiveData
private var mSort: Sortable = Sortable.ID
private var mOrder: Order = Order.ASCENDING
private val observer = Observer<List<ShiftObject>> {
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<ShiftObject>.applyFilters(): List<ShiftObject> {
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<ShiftObject>.sortList(sort: Sortable, order: Order): List<ShiftObject> {
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<Sortable, Order> {
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
}
}

View File

@@ -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]
)
}
}

View File

@@ -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
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zm1,15h-2v-6h2v6zm0,-8h-2V7h2v2z" />
</vector>

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -1,74 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
android:height="108dp"
android:width="108dp"
android:viewportHeight="108"
android:viewportWidth="108"
xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z"/>
<path android:fillColor="#00000000" android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
<path android:fillColor="#00000000" android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/>
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11.5,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zm6.5,-6v-5.5c0,-3.07 -2.13,-5.64 -5,-6.32V3.5c0,-0.83 -0.67,-1.5 -1.5,-1.5S10,2.67 10,3.5v0.68c-2.87,0.68 -5,3.25 -5,6.32V16l-2,2v1h17v-1l-2,-2z" />
</vector>

View File

@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01,-0.25 1.97,-0.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0,-4.42,-3.58,-8,-8,-8zm0 14c-3.31 0,-6,-2.69,-6,-6 0,-1.01 0.25,-1.97 0.7,-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4,-4,-4,-4v3z" />
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto">
<RelativeLayout
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<TextView
android:id="@+id/empty_title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:fontFamily="sans-serif-medium"
android:paddingTop="16dp"
android:text="Shift list empty"
android:textAppearance="?android:textAppearanceMedium"/>
<TextView
android:id="@+id/empty_subtitle_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/empty_title_text"
android:layout_centerHorizontal="true"
android:fontFamily="sans-serif"
android:paddingTop="8dp"
android:text="add shift to begin"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="#A2AAB0"/>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -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">
<ProgressBar
android:id="@+id/pd_ai"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="gone"/>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
@@ -189,7 +182,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Break"
android:text="@string/break_res"
android:textAppearance="@style/TextAppearance.AppCompat" />
<EditText
@@ -198,7 +191,7 @@
android:layout_height="wrap_content"
android:layout_weight="2"
android:ems="10"
android:hint="Break in minutes"
android:hint="@string/insert_break_in_minutes"
android:inputType="number"
android:selectAllOnFocus="true" />
</LinearLayout>

View File

@@ -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">
<LinearLayout
android:layout_width="match_parent"

View File

@@ -7,7 +7,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.FurtherInfoFragment">
tools:context="com.appttude.h_mal.farmr.ui.FurtherInfoFragment">
<ProgressBar
android:visibility="gone"

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/recycler">
</androidx.recyclerview.widget.RecyclerView>
</FrameLayout>

View File

@@ -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">
<ListView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_item_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/list_item_1">
</ListView>
<RelativeLayout
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true">
<TextView
android:id="@+id/empty_title_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:fontFamily="sans-serif-medium"
android:paddingTop="16dp"
android:text="Shift list empty"
android:textAppearance="?android:textAppearanceMedium"/>
<TextView
android:id="@+id/empty_subtitle_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/empty_title_text"
android:layout_centerHorizontal="true"
android:fontFamily="sans-serif"
android:paddingTop="8dp"
android:text="add shift to begin"
android:textAppearance="?android:textAppearanceSmall"
android:textColor="#A2AAB0"/>
</RelativeLayout>
</androidx.recyclerview.widget.RecyclerView>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab1"
@@ -49,6 +21,11 @@
android:layout_alignParentBottom="true"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/add"
ads:backgroundTint="@color/colorPrimary" />
app:backgroundTint="@color/colorPrimary" />
<include
android:visibility="gone"
layout="@layout/empty_list_view"
android:id="@+id/empty_view"/>
</RelativeLayout>

View File

@@ -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" />

View File

@@ -2,7 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".MainActivity"
tools:context=".ui.MainActivity"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

View File

@@ -1,7 +1,7 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:context="com.appttude.h_mal.farmr.MainActivity">
tools:context="com.appttude.h_mal.farmr.ui.MainActivity">
<item
android:id="@+id/action_favorite"
android:icon="@drawable/image_i_64"

View File

@@ -1,11 +1,9 @@
<resources>
<string name="app_name">Farmr</string>
<string name="action_settings">Settings</string>
<string name="category_ach">Shifts</string>
<string name="add_item_title">Add Shift</string>
<string name="edit_item_title">Edit Shift</string>
<string name="delete_item">Delete Shift</string>
<string name="insert_item_failed">failed to insert Shift</string>
<string name="insert_item_successful">Shift successfully added</string>
<string name="update_item_failed">Update Failed</string>
@@ -25,11 +23,6 @@
<!-- Example General settings -->
<string name="pref_header_general">General</string>
<string name="pref_title_social_recommendations">Enable social recommendations</string>
<string name="pref_description_social_recommendations">Recommendations for people to contact
based on your message history
</string>
<string name="pref_title_display_name">Display name</string>
<string name="pref_default_display_name">John Smith</string>
@@ -102,4 +95,6 @@
<!-- TODO: Remove or change this placeholder text -->
<string name="hello_blank_fragment">Hello blank fragment</string>
<string name="further_info_title">Shift Details</string>
<string name="insert_break_in_minutes">insert break in minutes</string>
<string name="break_res">Break</string>
</resources>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="external_files" path="."/>
</paths>

View File

@@ -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 <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect() throws Exception {
assertEquals(4, 2 + 2);
}
}

View File

@@ -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<ShiftObject>(
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<List<ShiftObject>>(result)
assertEquals(result.first().id, anyLong())
}
}

View File

@@ -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 <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
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
),
)

View File

@@ -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<InfoViewModel>() {
@Test
fun retrieveData_validBundleAndId_successfulRetrieval() {
// Arrange
val id = anyLong()
val shift = mockk<ShiftObject>()
val bundle = mockk<Bundle>()
// Act
every { repository.readSingleShiftFromDatabase(id) }.returns(shift)
every { bundle.getLong(ID) }.returns(id)
viewModel.retrieveData(bundle)
// Assert
assertIs<ShiftObject>(retrieveCurrentData())
assertEquals(
retrieveCurrentData(),
shift
)
}
@Test
fun retrieveData_noValidBundleAndId_unsuccessfulRetrieval() {
// Arrange
val id = anyLong()
val shift = mockk<ShiftObject>()
val bundle = mockk<Bundle>()
// 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<Bundle>()
// 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
)
}
}

Some files were not shown because too many files have changed in this diff Show More