├── .gitignore
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── nerdery
│ │ └── rtaza
│ │ └── jobtracker
│ │ ├── data
│ │ ├── core
│ │ │ ├── Error.kt
│ │ │ ├── Resource.kt
│ │ │ └── Status.kt
│ │ ├── local
│ │ │ ├── ApplicationDatabase.kt
│ │ │ ├── dao
│ │ │ │ ├── JobDao.kt
│ │ │ │ ├── JobLocalDataSource.kt
│ │ │ │ ├── WorkerDao.kt
│ │ │ │ └── WorkerLocalDataSource.kt
│ │ │ └── model
│ │ │ │ ├── Customer.kt
│ │ │ │ ├── Job.kt
│ │ │ │ ├── JobStatus.kt
│ │ │ │ ├── JobTask.kt
│ │ │ │ ├── JobWithRelations.kt
│ │ │ │ ├── Location.kt
│ │ │ │ └── Worker.kt
│ │ ├── remote
│ │ │ ├── HttpConstants.kt
│ │ │ ├── adapter
│ │ │ │ └── RxErrorHandlingCallAdapterFactory.kt
│ │ │ ├── api
│ │ │ │ └── JobWebApi.kt
│ │ │ └── model
│ │ │ │ ├── CustomerResponse.kt
│ │ │ │ ├── JobResponse.kt
│ │ │ │ ├── LocationResponse.kt
│ │ │ │ └── WorkerResponse.kt
│ │ ├── repository
│ │ │ ├── JobRepository.kt
│ │ │ ├── NetworkCall.kt
│ │ │ ├── NetworkResourceCall.kt
│ │ │ └── PersistableNetworkResourceCall.kt
│ │ └── util
│ │ │ └── JobResponseFactory.kt
│ │ ├── di
│ │ ├── ActivityScope.kt
│ │ ├── ViewModelFactory.kt
│ │ ├── ViewModelKey.kt
│ │ ├── component
│ │ │ └── ApplicationComponent.kt
│ │ └── module
│ │ │ ├── AdapterModule.kt
│ │ │ ├── ApplicationModule.kt
│ │ │ ├── HttpModule.kt
│ │ │ ├── PersistenceModule.kt
│ │ │ └── ViewModelModule.kt
│ │ ├── log
│ │ └── LogUtil.kt
│ │ └── ui
│ │ ├── JobTrackerApplication.kt
│ │ ├── core
│ │ ├── BaseActivity.kt
│ │ ├── BaseViewModel.kt
│ │ └── SingleLiveEvent.kt
│ │ ├── jobs
│ │ ├── JobViewHolder.kt
│ │ ├── JobsActivity.kt
│ │ ├── JobsListAdapter.kt
│ │ └── JobsViewModel.kt
│ │ ├── util
│ │ ├── CollectionExtensions.kt
│ │ ├── ContextExtensions.kt
│ │ ├── JobIconUtil.kt
│ │ ├── LiveDataExtensions.kt
│ │ ├── StringExtensions.kt
│ │ ├── TextFormatter.kt
│ │ ├── TimeUtil.kt
│ │ ├── ViewExtensions.kt
│ │ └── Visibility.kt
│ │ └── view
│ │ └── OffsetItemDecoration.kt
│ └── res
│ ├── drawable-v24
│ └── foreground_launch.xml
│ ├── drawable
│ ├── background_launch.xml
│ ├── icon_background_job_status.xml
│ ├── icon_carpentry.xml
│ ├── icon_electrical.xml
│ ├── icon_error_client_io.xml
│ ├── icon_error_server.xml
│ ├── icon_error_uauthorized.xml
│ ├── icon_error_unknown.xml
│ ├── icon_hvac.xml
│ ├── icon_no_active_jobs.xml
│ ├── icon_paint.xml
│ └── icon_plumbing.xml
│ ├── layout
│ ├── activity_jobs.xml
│ └── item_job.xml
│ ├── mipmap-anydpi-v26
│ ├── icon_launcher.xml
│ └── icon_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── icon_launcher.png
│ └── icon_launcher_round.png
│ ├── mipmap-mdpi
│ ├── icon_launcher.png
│ └── icon_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── icon_launcher.png
│ └── icon_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── icon_launcher.png
│ └── icon_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── icon_launcher.png
│ └── icon_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ ├── styles.xml
│ └── themes.xml
├── build.gradle
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── schemas
└── com.nerdery.rtaza.jobtracker.data.local.ApplicationDatabase
│ └── 3.json
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | # Built application files
2 | *.apk
3 | *.ap_
4 |
5 | # Files for the ART/Dalvik VM
6 | *.dex
7 |
8 | # Java class files
9 | *.class
10 |
11 | # Generated files
12 | bin/
13 | gen/
14 | out/
15 |
16 | # Gradle files
17 | .gradle/
18 | build/
19 |
20 | # Local configuration file (sdk path, etc)
21 | local.properties
22 |
23 | # Proguard folder generated by Eclipse
24 | proguard/
25 |
26 | # Log Files
27 | *.log
28 |
29 | # Android Studio Navigation editor temp files
30 | .navigation/
31 |
32 | # Android Studio captures folder
33 | captures/
34 |
35 | # Intellij
36 | *.iml
37 | .idea/
38 |
39 | # Keystore files
40 | *.jks
41 |
42 | # External native build folder generated in Android Studio 2.2 and later
43 | .externalNativeBuild
44 |
45 | # Google Services (e.g. APIs or Firebase)
46 | google-services.json
47 |
48 | # Freeline
49 | freeline.py
50 | freeline/
51 | freeline_project_description.json
52 |
53 | # OS-specific
54 | .DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # JobTracker
2 | [Medium article](https://medium.com/@rtaza/android-mvvm-with-network-status-observables-ft-livedata-rxjava-room-listadapter-and-kotlin-a54c328c707b)
3 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "kotlin-android"
3 | apply plugin: "kotlin-android-extensions"
4 | apply plugin: "kotlin-kapt"
5 |
6 | android {
7 | compileSdkVersion 28
8 | defaultConfig {
9 | applicationId "com.nerdery.rtaza.jobtracker"
10 | minSdkVersion 21
11 | targetSdkVersion 28
12 | versionCode 1
13 | versionName "1.0"
14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
20 | }
21 | }
22 | flavorDimensions "default"
23 | productFlavors {
24 | sandbox {
25 | buildConfigField 'String', 'API_BASE_URL', '"https://demo/"'
26 | buildConfigField 'String', 'ENVIRONMENT', '"sandbox"'
27 | }
28 | }
29 | defaultConfig {
30 | kapt {
31 | arguments {
32 | arg("room.schemaLocation", "$projectDir/../schemas".toString())
33 | }
34 | }
35 | }
36 | }
37 |
38 | dependencies {
39 | implementation fileTree(include: ["*.jar"], dir: "libs")
40 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"
41 | implementation "androidx.appcompat:appcompat:$appCompatVersion"
42 | implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
43 | implementation "androidx.cardview:cardview:$cardViewVersion"
44 | implementation "com.google.android.material:material:$materialVersion"
45 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayoutVersion"
46 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
47 | kapt "androidx.lifecycle:lifecycle-compiler:$lifecycleVersion"
48 | implementation "androidx.room:room-runtime:$roomVersion"
49 | kapt "androidx.room:room-compiler:$roomVersion"
50 | implementation "androidx.room:room-rxjava2:$roomVersion"
51 | implementation "com.squareup.okhttp3:logging-interceptor:$okHttpVersion"
52 | implementation "com.squareup.okhttp3:okhttp:$okHttpVersion"
53 | implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
54 | implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
55 | implementation "com.squareup.retrofit2:converter-moshi:$retrofitVersion"
56 | implementation "com.squareup.moshi:moshi-kotlin:$moshiVersion"
57 | implementation "io.reactivex.rxjava2:rxandroid:$rxAndroidVersion"
58 | implementation "io.reactivex.rxjava2:rxkotlin:$rxKotlinVersion"
59 | implementation "com.google.dagger:dagger:$daggerVersion"
60 | kapt "com.google.dagger:dagger-compiler:$daggerVersion"
61 | implementation "com.google.dagger:dagger-android:$daggerVersion"
62 | implementation "com.google.dagger:dagger-android-support:$daggerVersion"
63 | kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
64 | implementation "com.jakewharton.timber:timber:$timberVersion"
65 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
15 |
16 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/core/Error.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.core
2 |
3 | import android.content.Context
4 | import androidx.annotation.DrawableRes
5 | import androidx.annotation.StringRes
6 | import com.nerdery.rtaza.jobtracker.R
7 | import java.net.HttpURLConnection
8 |
9 | const val CODE_UNKNOWN = -1
10 | const val CODE_CLIENT_IO = 50
11 |
12 | /**
13 | * Classification of all errors known to the application.
14 | */
15 | sealed class Error(
16 | @DrawableRes val iconResourceId: Int,
17 | @StringRes val titleResourceId: Int,
18 | @StringRes val descriptionResourceId: Int,
19 | val code: Int,
20 | throwable: Throwable?
21 | ) : kotlin.Exception(throwable?.message, throwable) {
22 |
23 | open fun getFullErrorMessage(context: Context): String = context.getString(
24 | R.string.format_error_title_description,
25 | context.getString(titleResourceId),
26 | context.getString(descriptionResourceId)
27 | )
28 |
29 | class HttpClientIo(throwable: Throwable) :
30 | Error(
31 | R.drawable.icon_error_client_io,
32 | R.string.error_title_http_io,
33 | R.string.error_description_http_io,
34 | CODE_CLIENT_IO,
35 | throwable
36 | )
37 |
38 | class HttpServer(throwable: Throwable) :
39 | Error(
40 | R.drawable.icon_error_server,
41 | R.string.error_title_http_server,
42 | R.string.error_description_http_server,
43 | HttpURLConnection.HTTP_INTERNAL_ERROR,
44 | throwable
45 | )
46 |
47 | class HttpUnauthorized(throwable: Throwable) :
48 | Error(
49 | R.drawable.icon_error_uauthorized,
50 | R.string.error_title_http_unauthorized,
51 | R.string.error_description_http_unauthorized,
52 | HttpURLConnection.HTTP_UNAUTHORIZED,
53 | throwable
54 | )
55 |
56 | class Unknown(throwable: Throwable? = null) :
57 | Error(
58 | R.drawable.icon_error_unknown,
59 | R.string.error_title_unknown,
60 | R.string.error_description_unknown,
61 | CODE_UNKNOWN,
62 | throwable
63 | ) {
64 |
65 | override fun getFullErrorMessage(context: Context): String = context.getString(R.string.error_title_unknown)
66 | }
67 |
68 | companion object {
69 |
70 | /**
71 | * Converts an error [code] to an [Error] instance.
72 | */
73 | fun getFromCode(code: Int, throwable: Throwable): Error {
74 | return when (code) {
75 | CODE_CLIENT_IO -> HttpClientIo(throwable)
76 | HttpURLConnection.HTTP_INTERNAL_ERROR -> HttpServer(throwable)
77 | HttpURLConnection.HTTP_UNAUTHORIZED -> HttpUnauthorized(throwable)
78 | else -> Unknown(throwable)
79 | }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/core/Resource.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.core
2 |
3 | /**
4 | * A wrapper for data that is requested from a data source.
5 | *
6 | * @param status the current status ([Status.LOADING], [Status.RESOURCE_FOUND], [Status.NO_RESOURCE_FOUND], or
7 | * [Status.ERROR]) of the Resource at any given time.
8 | * @param data the object that the Resource wraps. [Status.LOADING] and [Status.RESOURCE_FOUND] could each have different
9 | * representations of the data.
10 | * @param error the [com.nerdery.rtaza.jobtracker.data.core.Error] that the Resource wraps in the case that the Resource
11 | * has a status of [Status.ERROR].
12 | */
13 | sealed class Resource(
14 | val status: Status,
15 | val data: DataType?,
16 | val error: com.nerdery.rtaza.jobtracker.data.core.Error?
17 | ) {
18 |
19 | class Loading(loadingData: DataType?) : Resource(Status.LOADING, loadingData, null)
20 |
21 | class ResourceFound(successData: DataType) : Resource(Status.RESOURCE_FOUND, successData, null)
22 |
23 | class NoResourceFound : Resource(Status.NO_RESOURCE_FOUND, null, null)
24 |
25 | class Error(error: com.nerdery.rtaza.jobtracker.data.core.Error) :
26 | Resource(Status.ERROR, null, error)
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/core/Status.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.core
2 |
3 | /**
4 | * Status of a network request to get a [Resource].
5 | */
6 | enum class Status {
7 | /**
8 | * The [Resource] is being loaded from a source.
9 | */
10 | LOADING,
11 | /**
12 | * The network request was successful, and the result is not null or empty.
13 | */
14 | RESOURCE_FOUND,
15 | /**
16 | * The network request was successful, but the result was null or empty.
17 | */
18 | NO_RESOURCE_FOUND,
19 | /**
20 | * The network request failed with an error.
21 | */
22 | ERROR
23 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/ApplicationDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import androidx.room.TypeConverters
6 | import com.nerdery.rtaza.jobtracker.data.local.dao.JobDao
7 | import com.nerdery.rtaza.jobtracker.data.local.dao.WorkerDao
8 | import com.nerdery.rtaza.jobtracker.data.local.model.Job
9 | import com.nerdery.rtaza.jobtracker.data.local.model.JobStatus
10 | import com.nerdery.rtaza.jobtracker.data.local.model.JobTask
11 | import com.nerdery.rtaza.jobtracker.data.local.model.Worker
12 |
13 | @Database(
14 | entities = [
15 | Job::class,
16 | Worker::class
17 | ], version = 3
18 | )
19 | @TypeConverters(
20 | JobStatus.Converter::class,
21 | JobTask.Converter::class
22 | )
23 | abstract class ApplicationDatabase : RoomDatabase() {
24 |
25 | abstract fun jobDao(): JobDao
26 |
27 | abstract fun workerDao(): WorkerDao
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/dao/JobDao.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.dao
2 |
3 | import androidx.room.*
4 | import com.nerdery.rtaza.jobtracker.data.local.model.Job
5 | import com.nerdery.rtaza.jobtracker.data.local.model.JobWithRelations
6 | import io.reactivex.Flowable
7 | import io.reactivex.Maybe
8 |
9 | private const val QUERY_SELECT_ALL = """
10 | SELECT job.*, worker.id as worker_id, worker.firstName as worker_firstName, worker.lastName as worker_lastName
11 | FROM job INNER JOIN worker ON job.workerId = worker.id
12 | WHERE active = :active
13 | ORDER BY status DESC, eta ASC"""
14 |
15 | private const val QUERY_JOB_ID = """
16 | SELECT job.*, worker.id as worker_id, worker.firstName as worker_firstName, worker.lastName as worker_lastName
17 | FROM job INNER JOIN worker ON job.workerId = worker.id
18 | WHERE job.id = :jobId"""
19 |
20 | /**
21 | * Data Access Object for defining Room database operations that can be performed on [Job] entities and entities related
22 | * to [Job]s, such as a [Job]'s assigned [com.nerdery.rtaza.jobtracker.data.local.model.Worker].
23 | */
24 | @Dao
25 | abstract class JobDao : JobLocalDataSource {
26 |
27 | @Insert(onConflict = OnConflictStrategy.REPLACE)
28 | abstract override fun insert(job: Job)
29 |
30 | @Insert(onConflict = OnConflictStrategy.REPLACE)
31 | abstract override fun insertAll(jobs: List)
32 |
33 | @Query(value = QUERY_JOB_ID)
34 | // Only queries the fields from worker that are pertinent to job-level details
35 | @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
36 | abstract override fun load(jobId: Long): Maybe
37 |
38 | @Query(value = QUERY_SELECT_ALL)
39 | // Only queries the fields from Worker that are pertinent to job-level details
40 | @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
41 | abstract override fun loadAll(active: Boolean): Maybe>
42 |
43 | override fun stream(jobId: Long): Flowable {
44 | return streamInternal(jobId).distinctUntilChanged()
45 | }
46 |
47 | @Query(value = QUERY_JOB_ID)
48 | // Only queries the fields from Worker that are pertinent to job-level details
49 | @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
50 | protected abstract fun streamInternal(jobId: Long): Flowable
51 |
52 | override fun streamAll(active: Boolean): Flowable> {
53 | return streamAllInternal(active).distinctUntilChanged()
54 | }
55 |
56 | @Query(value = QUERY_SELECT_ALL)
57 | // Only queries the fields from worker that are pertinent to job-level details
58 | @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH)
59 | protected abstract fun streamAllInternal(active: Boolean): Flowable>
60 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/dao/JobLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.dao
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Job
4 | import com.nerdery.rtaza.jobtracker.data.local.model.JobWithRelations
5 | import io.reactivex.Flowable
6 | import io.reactivex.Maybe
7 |
8 | interface JobLocalDataSource {
9 |
10 | fun insert(job: Job)
11 |
12 | fun insertAll(jobs: List)
13 |
14 | fun load(jobId: Long): Maybe
15 |
16 | fun loadAll(active: Boolean): Maybe>
17 |
18 | fun stream(jobId: Long): Flowable
19 |
20 | fun streamAll(active: Boolean): Flowable>
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/dao/WorkerDao.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.dao
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.nerdery.rtaza.jobtracker.data.local.model.Worker
8 | import io.reactivex.Flowable
9 | import io.reactivex.Maybe
10 |
11 | private const val QUERY_SELECT_ALL = "SELECT * FROM worker WHERE id = :workerId LIMIT 1"
12 |
13 | /**
14 | * Data Access Object for defining Room database operations that can be performed on [Worker] entities.
15 | */
16 | @Dao
17 | abstract class WorkerDao : WorkerLocalDataSource {
18 |
19 | @Insert(onConflict = OnConflictStrategy.REPLACE)
20 | abstract override fun insert(worker: Worker)
21 |
22 | @Insert(onConflict = OnConflictStrategy.REPLACE)
23 | abstract override fun insertAll(workers: List)
24 |
25 | @Query(QUERY_SELECT_ALL)
26 | abstract override fun load(workerId: String): Maybe
27 |
28 | override fun stream(workerId: String): Flowable> {
29 | return streamInternal(workerId).distinctUntilChanged()
30 | }
31 |
32 | @Query(QUERY_SELECT_ALL)
33 | protected abstract fun streamInternal(workerId: String): Flowable>
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/dao/WorkerLocalDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.dao
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Worker
4 | import io.reactivex.Flowable
5 | import io.reactivex.Maybe
6 |
7 | interface WorkerLocalDataSource {
8 |
9 | fun insert(worker: Worker)
10 |
11 | fun insertAll(workers: List)
12 |
13 | fun load(workerId: String): Maybe
14 |
15 | fun stream(workerId: String): Flowable>
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/Customer.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | data class Customer(
4 | val firstName: String,
5 | val lastName: String,
6 | val phoneNumber: String
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/Job.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | import androidx.room.*
4 |
5 | @Entity(
6 | foreignKeys = [(ForeignKey(
7 | entity = Worker::class,
8 | parentColumns = arrayOf("id"),
9 | childColumns = arrayOf("workerId")
10 | ))],
11 | indices = [(Index("workerId"))]
12 | )
13 | data class Job(
14 | @PrimaryKey var id: Long,
15 | var task: JobTask,
16 | private var status: JobStatus,
17 | private var active: Boolean,
18 | @Embedded var customer: Customer,
19 | @Embedded var location: Location,
20 | var workerId: Long?,
21 | var eta: Long,
22 | var notes: String?
23 | ) {
24 |
25 | fun setStatus(status: JobStatus) {
26 | this.status = status
27 | active = status.active
28 | }
29 |
30 | fun getStatus() = status
31 |
32 | fun getActive() = active
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/JobStatus.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.room.TypeConverter
5 | import com.nerdery.rtaza.jobtracker.R
6 |
7 | /**
8 | * The status of a job. The order of the enum values are significant. The ordinal of each value corresponds to the
9 | * priority of the job. The smaller the ordinal, the lower the priority.
10 | */
11 | enum class JobStatus(
12 | val id: String,
13 | @StringRes val label: Int,
14 | val active: Boolean
15 | ) {
16 | NEW("new", R.string.job_status_new, false),
17 | ACCEPTED("accepted", R.string.job_status_accepted, true),
18 | DECLINED("declined", R.string.job_status_declined, false),
19 | MISSED("missed", R.string.job_status_missed, false),
20 | EN_ROUTE("enRoute", R.string.job_status_en_route, true),
21 | ON_SITE("onSite", R.string.job_status_on_site, true),
22 | COMPLETED("completed", R.string.job_status_completed, false);
23 |
24 | class Converter {
25 |
26 | fun toJobStatus(id: String): JobStatus {
27 | var status = NEW
28 |
29 | for (value in JobStatus.values()) {
30 | if (value.id == id) {
31 | status = value
32 | }
33 | }
34 |
35 | return status
36 | }
37 |
38 | @TypeConverter
39 | fun toJobStatus(ordinal: Int): JobStatus {
40 | return values()[ordinal]
41 | }
42 |
43 | @TypeConverter
44 | fun toOrdinal(status: JobStatus): Int {
45 | return status.ordinal
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/JobTask.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | import androidx.annotation.StringRes
4 | import androidx.room.TypeConverter
5 | import com.nerdery.rtaza.jobtracker.R
6 |
7 | enum class JobTask(
8 | val id: String,
9 | @StringRes val label: Int
10 | ) {
11 | ELECTRICAL("electrical", R.string.job_task_electrical),
12 | HVAC("hvac", R.string.job_task_hvac),
13 | PLUMBING("plumbing", R.string.job_task_plumbing),
14 | CARPENTRY("carpentry", R.string.job_task_carpentry),
15 | PAINT("paint", R.string.job_task_paint);
16 |
17 | class Converter {
18 |
19 | fun toJobTask(id: String): JobTask {
20 | var task = PAINT
21 |
22 | for (value in JobTask.values()) {
23 | if (value.id == id) {
24 | task = value
25 | }
26 | }
27 |
28 | return task
29 | }
30 |
31 | @TypeConverter
32 | fun toJobTask(ordinal: Int): JobTask {
33 | return values()[ordinal]
34 | }
35 |
36 | @TypeConverter
37 | fun toOrdinal(task: JobTask): Int {
38 | return task.ordinal
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/JobWithRelations.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | import androidx.room.Embedded
4 |
5 | data class JobWithRelations(
6 | @Embedded var job: Job,
7 | @Embedded(prefix = "worker_") var worker: WorkerRelation?
8 | )
9 |
10 | data class WorkerRelation(
11 | val id: Long,
12 | val firstName: String,
13 | val lastName: String
14 | ) {
15 | constructor(worker: Worker) : this(worker.id, worker.firstName, worker.lastName)
16 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/Location.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | data class Location(
4 | val street: String,
5 | val city: String,
6 | val zip: String,
7 | val state: String,
8 | val latitude: Double,
9 | val longitude: Double
10 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/local/model/Worker.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.local.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity
7 | data class Worker(
8 | @PrimaryKey var id: Long,
9 | var firstName: String,
10 | var lastName: String,
11 | var online: Boolean
12 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/HttpConstants.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | class HttpConstants {
6 |
7 | companion object {
8 | const val TIMEOUT_IO: Long = 10_000
9 | val TIMEOUT_TIME_UNIT: TimeUnit = TimeUnit.MILLISECONDS
10 | }
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/adapter/RxErrorHandlingCallAdapterFactory.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.adapter
2 |
3 | import com.nerdery.rtaza.jobtracker.data.core.Error
4 | import io.reactivex.*
5 | import retrofit2.Call
6 | import retrofit2.CallAdapter
7 | import retrofit2.HttpException
8 | import retrofit2.Retrofit
9 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
10 | import timber.log.Timber
11 | import java.io.IOException
12 | import java.lang.reflect.Type
13 | import java.net.HttpURLConnection
14 |
15 | /**
16 | * Retrofit CallAdapter.Factory for centrally adapting all Retrofit exceptions to custom exceptions that contain
17 | * app-specific error codes and messages.
18 | */
19 | class RxErrorHandlingCallAdapterFactory(scheduler: Scheduler) : CallAdapter.Factory() {
20 | private val rxJava2CallAdapterFactory by lazy {
21 | RxJava2CallAdapterFactory.createWithScheduler(scheduler)
22 | }
23 |
24 | override fun get(returnType: Type, annotations: Array, retrofit: Retrofit): CallAdapter<*, *>? {
25 | return RxCallAdapterWrapper(
26 | rxJava2CallAdapterFactory.get(returnType, annotations, retrofit) as CallAdapter
27 | )
28 | }
29 |
30 | private class RxCallAdapterWrapper(private val rxJava2CallAdapter: CallAdapter) : CallAdapter {
31 |
32 | override fun responseType(): Type {
33 | return rxJava2CallAdapter.responseType()
34 | }
35 |
36 | override fun adapt(call: Call): Any {
37 | val stream = rxJava2CallAdapter.adapt(call)
38 |
39 | return when (stream) {
40 | is Flowable<*> -> stream.onErrorResumeNext { throwable: Throwable ->
41 | Flowable.error(adaptException(throwable))
42 | }
43 | is Single<*> -> stream.onErrorResumeNext { throwable ->
44 | Single.error(adaptException(throwable))
45 | }
46 | is Maybe<*> -> stream.onErrorResumeNext { throwable: Throwable ->
47 | Maybe.error(adaptException(throwable))
48 | }
49 | is Completable -> stream.onErrorResumeNext { throwable: Throwable ->
50 | Completable.error(adaptException(throwable))
51 | }
52 | is Observable<*> -> stream.onErrorResumeNext { throwable: Throwable ->
53 | Observable.error(adaptException(throwable))
54 | }
55 | else -> {
56 | Timber.wtf("Unknown RxJava stream type")
57 | stream
58 | }
59 | }
60 | }
61 |
62 | private fun adaptException(throwable: Throwable): Error {
63 | Timber.e(throwable)
64 |
65 | return when (throwable) {
66 | is IOException ->
67 | // Network error (no network connection, timeout)
68 | Error.HttpClientIo(throwable)
69 | is HttpException ->
70 | // HTTP error (server down, unauthorized request)
71 | when (throwable.code()) {
72 | HttpURLConnection.HTTP_INTERNAL_ERROR -> Error.HttpServer(throwable)
73 | HttpURLConnection.HTTP_UNAUTHORIZED -> Error.HttpUnauthorized(throwable)
74 | else -> Error.Unknown(throwable)
75 | }
76 | else ->
77 | // Unknown error
78 | Error.Unknown(throwable)
79 | }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/api/JobWebApi.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.api
2 |
3 | import com.nerdery.rtaza.jobtracker.data.remote.model.JobResponse
4 | import io.reactivex.Single
5 | import retrofit2.http.GET
6 | import retrofit2.http.Query
7 |
8 | interface JobWebApi {
9 |
10 | @GET("job")
11 | fun fetch(@Query("jobId") jobId: Long): Single
12 |
13 | @GET("jobs")
14 | fun fetchAll(@Query("active") active: Boolean): Single>
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/model/CustomerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.model
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Customer
4 |
5 | data class CustomerResponse(
6 | val firstName: String,
7 | val lastName: String,
8 | val phoneNumber: String
9 | ) {
10 |
11 | fun toCustomer(): Customer {
12 | return Customer(firstName, lastName, phoneNumber)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/model/JobResponse.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.model
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Job
4 | import com.nerdery.rtaza.jobtracker.data.local.model.JobStatus
5 | import com.nerdery.rtaza.jobtracker.data.local.model.JobTask
6 |
7 | data class JobResponse(
8 | val id: Long,
9 | val task: String,
10 | val status: String,
11 | val locationResponse: LocationResponse,
12 | val workerResponse: WorkerResponse?,
13 | val customerResponse: CustomerResponse,
14 | val eta: Long,
15 | val customerNotes: String?
16 | ) {
17 |
18 | fun toJob(): Job {
19 | val jobStatus = JobStatus.Converter().toJobStatus(status)
20 | return Job(
21 | id,
22 | JobTask.Converter().toJobTask(task),
23 | jobStatus,
24 | jobStatus.active,
25 | customerResponse.toCustomer(),
26 | locationResponse.toLocation(),
27 | workerResponse?.id,
28 | eta,
29 | customerNotes
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/model/LocationResponse.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.model
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Location
4 |
5 | data class LocationResponse(
6 | val street: String,
7 | val city: String,
8 | val zip: String,
9 | val state: String,
10 | val latitude: Double,
11 | val longitude: Double
12 | ) {
13 |
14 | fun toLocation(): Location {
15 | return Location(street, city, zip, state, latitude, longitude)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/remote/model/WorkerResponse.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.remote.model
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.Worker
4 |
5 | data class WorkerResponse(
6 | val id: Long,
7 | val firstName: String,
8 | val lastName: String,
9 | val available: Boolean
10 | ) {
11 |
12 | fun toWorker(): Worker {
13 | return Worker(id, firstName, lastName, available)
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/repository/JobRepository.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.repository
2 |
3 | import com.nerdery.rtaza.jobtracker.data.core.Error
4 | import com.nerdery.rtaza.jobtracker.data.core.Resource
5 | import com.nerdery.rtaza.jobtracker.data.local.dao.JobLocalDataSource
6 | import com.nerdery.rtaza.jobtracker.data.local.dao.WorkerLocalDataSource
7 | import com.nerdery.rtaza.jobtracker.data.local.model.JobWithRelations
8 | import com.nerdery.rtaza.jobtracker.data.remote.api.JobWebApi
9 | import com.nerdery.rtaza.jobtracker.data.remote.model.JobResponse
10 | import com.nerdery.rtaza.jobtracker.data.util.JobResponseFactory
11 | import com.nerdery.rtaza.jobtracker.ui.util.isNullOrEmpty
12 | import io.reactivex.Maybe
13 | import io.reactivex.Observable
14 | import io.reactivex.ObservableEmitter
15 | import io.reactivex.Single
16 | import javax.inject.Inject
17 |
18 | class JobRepository @Inject constructor(
19 | private val jobRemoteDataSource: JobWebApi,
20 | private val jobLocalDataSource: JobLocalDataSource,
21 | private val workerLocalDataSource: WorkerLocalDataSource
22 | ) {
23 |
24 | fun getJobs(active: Boolean): Observable>> {
25 | return object : PersistableNetworkResourceCall, List>() {
26 | override fun loadFromDatabase(): Maybe> = jobLocalDataSource.loadAll(active)
27 |
28 | override fun createNetworkCall(): Single> = jobRemoteDataSource.fetchAll(active)
29 |
30 | override fun onNetworkCallSuccess(
31 | emitter: ObservableEmitter>>,
32 | response: List
33 | ) {
34 | handleNetworkResponse(emitter, response, active)
35 | }
36 |
37 | // The network request is expected to fail since the URL that's being hit isn't valid.
38 | // Insert some fake data here for now.
39 | // TODO: Remove this method override after a local or remote data server is set up to return data
40 | override fun onNetworkCallError(
41 | emitter: ObservableEmitter>>,
42 | error: Error
43 | ) {
44 | val response = listOf(
45 | JobResponseFactory.createAcceptedHvacJobResponse(),
46 | JobResponseFactory.createAcceptedElectricalJobResponse(),
47 | JobResponseFactory.createEnRoutePlumbingJobResponse()
48 | )
49 |
50 | handleNetworkResponse(emitter, response, active)
51 | }
52 | }.resourceObservable
53 | }
54 |
55 | fun handleNetworkResponse(
56 | emitter: ObservableEmitter>>, response: List,
57 | active: Boolean
58 | ) {
59 | if (response.isNullOrEmpty()) {
60 | emitter.onNext(Resource.NoResourceFound())
61 | } else {
62 | updateLocalDataSources(response)
63 | emitter.setDisposable(jobLocalDataSource.streamAll(active)
64 | .subscribe { jobsWithRelations ->
65 | emitter.onNext(Resource.ResourceFound(jobsWithRelations))
66 | })
67 | }
68 | }
69 |
70 | fun getJob(jobId: Long): Observable> {
71 | return object : PersistableNetworkResourceCall() {
72 | override fun loadFromDatabase(): Maybe = jobLocalDataSource.load(jobId)
73 |
74 | override fun createNetworkCall(): Single = jobRemoteDataSource.fetch(jobId)
75 |
76 | override fun onNetworkCallSuccess(
77 | emitter: ObservableEmitter>,
78 | response: JobResponse
79 | ) {
80 | updateLocalDataSources(response)
81 | emitter.setDisposable(jobLocalDataSource.stream(jobId)
82 | .subscribe { jobWithRelations ->
83 | emitter.onNext(Resource.ResourceFound(jobWithRelations))
84 | })
85 | }
86 | }.resourceObservable
87 | }
88 |
89 | private fun updateLocalDataSources(jobsResponse: List) {
90 | // Pull out all non-null worker entities assigned to these jobs so they can be inserted into the worker database
91 | val workers = jobsResponse.map { workerResponse ->
92 | workerResponse.workerResponse
93 | }.mapNotNull { workerResponse ->
94 | workerResponse?.toWorker()
95 | }
96 |
97 | workerLocalDataSource.insertAll(workers)
98 | jobLocalDataSource.insertAll(jobsResponse.map { jobResponse ->
99 | jobResponse.toJob()
100 | })
101 | }
102 |
103 | private fun updateLocalDataSources(jobResponse: JobResponse) {
104 | // Pull out the worker entity assigned to this job (if it's not null) so it can be inserted into the worker database
105 | jobResponse.workerResponse?.let { workerResponse ->
106 | workerLocalDataSource.insert(workerResponse.toWorker())
107 | }
108 | jobLocalDataSource.insert(jobResponse.toJob())
109 | }
110 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/repository/NetworkCall.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.repository
2 |
3 | import android.annotation.SuppressLint
4 | import com.nerdery.rtaza.jobtracker.data.core.Error
5 | import io.reactivex.ObservableEmitter
6 | import io.reactivex.Single
7 | import timber.log.Timber
8 |
9 | /**
10 | * Abstracts the logic for making a network call. This class forwards the request's result payload or error to
11 | * [onNetworkCallSuccess] or [onNetworkCallError], respectively.
12 | *
13 | * @param ResponseType type of the response returned from the network call.
14 | * @param EmissionType type expected by the [ObservableEmitter].
15 | */
16 | abstract class NetworkCall {
17 |
18 | /**
19 | * Override this method and return the response of the network call.
20 | *
21 | * @return the [Single] from the network call stream.
22 | */
23 | protected abstract fun createNetworkCall(): Single
24 |
25 | /**
26 | * Override this method to handle the success response of the network call.
27 | *
28 | * @param emitter the [ObservableEmitter] of the calling [io.reactivex.Observable]. This is passed as a param to
29 | * allow subclasses to override the flow of the [io.reactivex.Observable] based on the response of this call.
30 | * @param response the success response of the network call.
31 | */
32 | protected abstract fun onNetworkCallSuccess(emitter: ObservableEmitter, response: ResponseType)
33 |
34 | /**
35 | * Override this method to handle the error returned from the network call.
36 | *
37 | * @param emitter the [ObservableEmitter] of the calling [io.reactivex.Observable]. This is passed as a param to
38 | * allow subclasses to override the flow of the [io.reactivex.Observable] based on the error from this call.
39 | * @param error the mapped [Error] thrown from the call.
40 | */
41 | protected abstract fun onNetworkCallError(emitter: ObservableEmitter, error: Error)
42 |
43 | /**
44 | * Performs the network call defined in [createNetworkCall].
45 | * This method should be called from within an [io.reactivex.Observable].
46 | *
47 | * @param emitter the [ObservableEmitter] of the calling [io.reactivex.Observable].
48 | */
49 | @SuppressLint("CheckResult") // The Single's result is disposed in the onSuccess or onError
50 | protected open fun fetchFromNetwork(emitter: ObservableEmitter) {
51 | createNetworkCall()
52 | .subscribe({ response ->
53 | onNetworkCallSuccess(emitter, response)
54 | }, { error ->
55 | if (error is Error) {
56 | onNetworkCallError(emitter, error)
57 | } else {
58 | // This should never happen since all network exceptions are caught and typecasted in
59 | // RxErrorHandlingCallAdapterFactory
60 | Timber.wtf("fetchFromNetwork() returned unknown error: $error")
61 | }
62 | })
63 | }
64 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/repository/NetworkResourceCall.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.repository
2 |
3 | import com.nerdery.rtaza.jobtracker.data.core.Error
4 | import com.nerdery.rtaza.jobtracker.data.core.Resource
5 | import io.reactivex.Observable
6 | import io.reactivex.ObservableEmitter
7 |
8 | /**
9 | * Abstracts the logic for making a network call that returns a resource.
10 | *
11 | * Step 1. Emits loading.
12 | * Step 2. Attempts to fetch item from network.
13 | * Step 3. If the network call fails, emits the [Error].
14 | *
15 | * All operations are executed on the TIMEOUT_IO [io.reactivex.Scheduler].
16 | *
17 | * @param ResourceType type of the resource to be emitted.
18 | */
19 | abstract class NetworkResourceCall : NetworkCall>() {
20 | val resourceObservable: Observable>
21 |
22 | init {
23 | resourceObservable = Observable.create> { emitter ->
24 | emitter.onNext(Resource.Loading(null))
25 | fetchFromNetwork(emitter)
26 | }
27 | }
28 |
29 | /**
30 | * Emits the [error] from the network call. Override this if need to handle the [error] differently.
31 | */
32 | override fun onNetworkCallError(emitter: ObservableEmitter>, error: Error) {
33 | emitter.onNext(Resource.Error(error))
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/repository/PersistableNetworkResourceCall.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.repository
2 |
3 | import com.nerdery.rtaza.jobtracker.data.core.Error
4 | import com.nerdery.rtaza.jobtracker.data.core.Resource
5 | import io.reactivex.Maybe
6 | import io.reactivex.Observable
7 | import io.reactivex.ObservableEmitter
8 | import timber.log.Timber
9 |
10 | /**
11 | * Abstracts the logic for making a network call that returns a resource that is persisted as a resource. Note that this
12 | * class handles two different data types because the data type returned from the web API, [ResponseType], may not match
13 | * the data type used in the local database, [ResourceType].
14 | *
15 | * Step 1. Emits loading.
16 | * Step 2. Attempts to load resource from local database.
17 | * Step 3. If load returns a resource, emits loading with that resource.
18 | * If load completes with an error, continues.
19 | * If load returns no resource, continues.
20 | * Step 4. Attempts to fetch item from network.
21 | * Step 5. If the network call fails, emits the [Error].
22 | *
23 | * All operations are executed on the TIMEOUT_IO [io.reactivex.Scheduler].
24 | *
25 | * @param ResponseType type of the response returned from the network call.
26 | * @param ResourceType type of the resource to be stored in the database and to be emitted.
27 | */
28 | abstract class PersistableNetworkResourceCall :
29 | NetworkCall>() {
30 | val resourceObservable: Observable>
31 |
32 | init {
33 | resourceObservable = Observable.create> { emitter ->
34 | emitter.onNext(Resource.Loading(null))
35 | loadFromDatabase()
36 | .subscribe({ queryResult ->
37 | emitter.onNext(Resource.Loading(queryResult))
38 | fetchFromNetwork(emitter)
39 | }, { error ->
40 | Timber.e(error)
41 | fetchFromNetwork(emitter)
42 | }, {
43 | fetchFromNetwork(emitter)
44 | })
45 | }
46 | }
47 |
48 | /**
49 | * Called to execute a local database query and return the result of that query.
50 | *
51 | * @return the result of the [Maybe] from the database query stream.
52 | */
53 | protected abstract fun loadFromDatabase(): Maybe
54 |
55 | /**
56 | * Emits the [error] from the network call. Override this if need to handle the [error] differently.
57 | */
58 | override fun onNetworkCallError(emitter: ObservableEmitter>, error: Error) {
59 | emitter.onNext(Resource.Error(error))
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/data/util/JobResponseFactory.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.data.util
2 |
3 | import com.nerdery.rtaza.jobtracker.data.local.model.JobStatus
4 | import com.nerdery.rtaza.jobtracker.data.local.model.JobTask
5 | import com.nerdery.rtaza.jobtracker.data.remote.model.CustomerResponse
6 | import com.nerdery.rtaza.jobtracker.data.remote.model.JobResponse
7 | import com.nerdery.rtaza.jobtracker.data.remote.model.LocationResponse
8 | import com.nerdery.rtaza.jobtracker.data.remote.model.WorkerResponse
9 | import com.nerdery.rtaza.jobtracker.ui.util.TimeUtil
10 | import java.util.concurrent.TimeUnit
11 |
12 | class JobResponseFactory {
13 |
14 | companion object {
15 |
16 | fun createAcceptedHvacJobResponse() = JobResponse(
17 | id = 123456789,
18 | task = JobTask.HVAC.id,
19 | status = JobStatus.ACCEPTED.id,
20 | locationResponse = LocationResponse(
21 | "6501 Railroad Ave",
22 | "Twin Peaks",
23 | "98065",
24 | "WA",
25 | 47.544390,
26 | -121.841370
27 | ),
28 | workerResponse = WorkerResponse(919345446, "Phillip", "Jeffries", true),
29 | customerResponse = CustomerResponse("Dale", "Cooper", "4306665421"),
30 | eta = TimeUtil.getCurrentTimeMillis() + TimeUnit.MINUTES.toMillis(15),
31 | customerNotes = "Diane, 7:30 am, February twenty-fourth. Entering town of Twin Peaks. Five miles south of the Canadian border, twelve miles west of the state line. Never seen so many trees in my life. As W.C. Fields would say, I'd rather be here than Philadelphia. It's fifty-four degrees on a slightly overcast day. Weatherman said rain. If you could get paid that kind of money for being wrong sixty percent of the time it'd beat working. Mileage is 79,345, gauge is on reserve, I'm riding on fumes here, I've got to tank up when I get into town. Remind me to tell you how much that is. Lunch was \$6.31 at the Lamplighter Inn. That's on Highway Two near Lewis Fork. That was a tuna fish sandwich on whole wheat, a slice of cherry pie and a cup of coffee. Damn good food. Diane, if you ever get up this way, that cherry pie is worth a stop."
32 | )
33 |
34 | fun createAcceptedElectricalJobResponse() = JobResponse(
35 | id = 987654321,
36 | task = JobTask.ELECTRICAL.id,
37 | status = JobStatus.ACCEPTED.id,
38 | locationResponse = LocationResponse(
39 | "137 W North Bend Way",
40 | "Twin Peaks",
41 | "98045",
42 | "WA",
43 | 47.494310,
44 | -121.785130
45 | ),
46 | workerResponse = WorkerResponse(919937023, "Pete", "Martell", true),
47 | customerResponse = CustomerResponse("Laura", "Palmer", "4306652451"),
48 | eta = TimeUtil.getCurrentTimeMillis() + TimeUnit.MINUTES.toMillis(1),
49 | customerNotes = "Nowhere... fast. And you're not coming."
50 | )
51 |
52 | fun createEnRoutePlumbingJobResponse() = JobResponse(
53 | id = 543216789,
54 | task = JobTask.PLUMBING.id,
55 | status = JobStatus.EN_ROUTE.id,
56 | locationResponse = LocationResponse(
57 | "4200 Preston-Fall City Rd",
58 | "Twin Peaks",
59 | "98066",
60 | "WA",
61 | 47.567310,
62 | -121.887550
63 | ),
64 | workerResponse = WorkerResponse(919012238, "Gordon", "Cole", true),
65 | customerResponse = CustomerResponse("Margaret", "Lanterman", "4306665421"),
66 | eta = TimeUtil.getCurrentTimeMillis() - TimeUnit.MINUTES.toMillis(1),
67 | customerNotes = "A log is a portion of a tree. (Turning end of log to camera.) At the end of a crosscut log — many of you know this — there are rings. Each ring represents one year in the life of the tree. How long it takes to a grow a tree! I don't mind telling you some things. Many things I, I musn't say. Just notice that my fireplace is boarded up. There will never be a fire there. On the mantelpiece, in that jar, are some of the ashes of my husband. My log hears things I cannot hear. But my log tells me about the sounds, about the new words. Even though it has stopped growing larger, my log is aware."
68 | )
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/ActivityScope.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di
2 |
3 | import javax.inject.Scope
4 |
5 | @Scope
6 | @Retention(AnnotationRetention.RUNTIME)
7 | annotation class ActivityScope
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/ViewModelFactory.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.ViewModelProvider
6 | import javax.inject.Inject
7 | import javax.inject.Provider
8 |
9 | /**
10 | * Factory responsible for creating all [ViewModel]s.
11 | */
12 | class ViewModelFactory @Inject constructor(
13 | private val creators: Map, @JvmSuppressWildcards Provider>,
14 | application: Application
15 | ) : ViewModelProvider.AndroidViewModelFactory(application) {
16 |
17 | override fun create(viewModelClass: Class): ViewModelType {
18 | var creator: Provider? = creators[viewModelClass]
19 | if (creator == null) {
20 | for ((key, value) in creators) {
21 | if (viewModelClass.isAssignableFrom(key)) {
22 | creator = value
23 | break
24 | }
25 | }
26 | }
27 |
28 | if (creator == null) throw IllegalArgumentException("Unknown ViewModel class $viewModelClass")
29 | @Suppress("UNCHECKED_CAST")
30 | return creator.get() as ViewModelType
31 | }
32 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/ViewModelKey.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di
2 |
3 | import androidx.lifecycle.ViewModel
4 | import dagger.MapKey
5 | import kotlin.reflect.KClass
6 |
7 | @Target(AnnotationTarget.FUNCTION)
8 | @Retention(AnnotationRetention.RUNTIME)
9 | @MapKey
10 | internal annotation class ViewModelKey(val value: KClass)
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/component/ApplicationComponent.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.component
2 |
3 | import android.app.Application
4 | import com.nerdery.rtaza.jobtracker.di.module.*
5 | import com.nerdery.rtaza.jobtracker.ui.JobTrackerApplication
6 | import dagger.BindsInstance
7 | import dagger.Component
8 | import dagger.android.AndroidInjectionModule
9 | import javax.inject.Singleton
10 |
11 | @Singleton
12 | @Component(
13 | modules = [
14 | AndroidInjectionModule::class,
15 | ApplicationModule::class,
16 | PersistenceModule::class,
17 | HttpModule::class,
18 | ViewModelModule::class,
19 | AdapterModule::class
20 | ]
21 | )
22 | interface ApplicationComponent {
23 |
24 | @Component.Builder
25 | interface Builder {
26 |
27 | @BindsInstance
28 | fun application(application: Application): Builder
29 |
30 | fun build(): ApplicationComponent
31 | }
32 |
33 | fun inject(application: JobTrackerApplication)
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/module/AdapterModule.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.module
2 |
3 | import com.nerdery.rtaza.jobtracker.ui.jobs.JobsListAdapter
4 | import dagger.Module
5 | import dagger.Provides
6 |
7 | @Module
8 | class AdapterModule {
9 |
10 | @Provides
11 | fun provideJobsListAdapter(): JobsListAdapter {
12 | return JobsListAdapter()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/module/ApplicationModule.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.module
2 |
3 | import com.nerdery.rtaza.jobtracker.di.ActivityScope
4 | import com.nerdery.rtaza.jobtracker.ui.jobs.JobsActivity
5 | import dagger.Module
6 | import dagger.android.ContributesAndroidInjector
7 |
8 | @Module
9 | abstract class ApplicationModule {
10 |
11 | @ActivityScope
12 | @ContributesAndroidInjector
13 | abstract fun contributeMainActivityInjector(): JobsActivity
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/module/HttpModule.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.module
2 |
3 | import com.nerdery.rtaza.jobtracker.BuildConfig
4 | import com.nerdery.rtaza.jobtracker.data.remote.HttpConstants
5 | import com.nerdery.rtaza.jobtracker.data.remote.adapter.RxErrorHandlingCallAdapterFactory
6 | import com.nerdery.rtaza.jobtracker.data.remote.api.JobWebApi
7 | import com.squareup.moshi.Moshi
8 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
9 | import dagger.Module
10 | import dagger.Provides
11 | import io.reactivex.schedulers.Schedulers
12 | import okhttp3.OkHttpClient
13 | import okhttp3.logging.HttpLoggingInterceptor
14 | import retrofit2.Retrofit
15 | import retrofit2.converter.moshi.MoshiConverterFactory
16 | import timber.log.Timber
17 |
18 | @Module
19 | class HttpModule {
20 |
21 | @Provides
22 | fun provideMoshi(): Moshi {
23 | return Moshi.Builder()
24 | .add(KotlinJsonAdapterFactory())
25 | .build()
26 | }
27 |
28 | @Provides
29 | fun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient {
30 | return OkHttpClient.Builder()
31 | .addInterceptor(loggingInterceptor)
32 | .connectTimeout(HttpConstants.TIMEOUT_IO, HttpConstants.TIMEOUT_TIME_UNIT)
33 | .readTimeout(HttpConstants.TIMEOUT_IO, HttpConstants.TIMEOUT_TIME_UNIT)
34 | .writeTimeout(HttpConstants.TIMEOUT_IO, HttpConstants.TIMEOUT_TIME_UNIT)
35 | .build()
36 | }
37 |
38 | @Provides
39 | fun provideRetrofit(httpClient: OkHttpClient, moshi: Moshi): Retrofit {
40 | return Retrofit.Builder()
41 | .baseUrl(BuildConfig.API_BASE_URL)
42 | .addCallAdapterFactory(RxErrorHandlingCallAdapterFactory(Schedulers.io()))
43 | .addConverterFactory(MoshiConverterFactory.create(moshi))
44 | .client(httpClient)
45 | .build()
46 | }
47 |
48 | @Provides
49 | fun provideHttpLoggingInterceptor(): HttpLoggingInterceptor {
50 | val loggingInterceptor = HttpLoggingInterceptor(HttpLoggingInterceptor.Logger { message ->
51 | Timber.tag("OkHttp")
52 | .d(message)
53 | })
54 | loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
55 | return loggingInterceptor
56 | }
57 |
58 | @Provides
59 | fun provideJobWebApi(retrofit: Retrofit): JobWebApi {
60 | return retrofit.create(JobWebApi::class.java)
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/module/PersistenceModule.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.module
2 |
3 | import android.app.Application
4 | import androidx.room.Room
5 | import com.nerdery.rtaza.jobtracker.data.local.ApplicationDatabase
6 | import com.nerdery.rtaza.jobtracker.data.local.dao.JobLocalDataSource
7 | import com.nerdery.rtaza.jobtracker.data.local.dao.WorkerLocalDataSource
8 | import dagger.Module
9 | import dagger.Provides
10 |
11 | @Module
12 | class PersistenceModule {
13 |
14 | @Provides
15 | fun provideRoomDatabase(application: Application): ApplicationDatabase {
16 | return Room.databaseBuilder(application, ApplicationDatabase::class.java, "job-tracker-database")
17 | .fallbackToDestructiveMigration()
18 | .build()
19 | }
20 |
21 | @Provides
22 | fun provideJobLocalDataSource(database: ApplicationDatabase): JobLocalDataSource {
23 | return database.jobDao()
24 | }
25 |
26 | @Provides
27 | fun provideWorkerLocalDataSource(database: ApplicationDatabase): WorkerLocalDataSource {
28 | return database.workerDao()
29 | }
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/di/module/ViewModelModule.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.di.module
2 |
3 | import androidx.lifecycle.ViewModel
4 | import androidx.lifecycle.ViewModelProvider
5 | import com.nerdery.rtaza.jobtracker.di.ViewModelFactory
6 | import com.nerdery.rtaza.jobtracker.di.ViewModelKey
7 | import com.nerdery.rtaza.jobtracker.ui.jobs.JobsViewModel
8 | import dagger.Binds
9 | import dagger.Module
10 | import dagger.multibindings.IntoMap
11 |
12 | @Module
13 | abstract class ViewModelModule {
14 |
15 | @Binds
16 | abstract fun bindViewModelFactory(viewModelFactory: ViewModelFactory): ViewModelProvider.Factory
17 |
18 | @Binds
19 | @IntoMap
20 | @ViewModelKey(JobsViewModel::class)
21 | abstract fun bindJobsViewModel(viewModel: JobsViewModel): ViewModel
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/log/LogUtil.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.log
2 |
3 | class LogUtil {
4 |
5 | companion object {
6 |
7 | /**
8 | * Returns the simple class name of [objectInstance].
9 | */
10 | fun getLogTag(objectInstance: Any): String {
11 | val clazz = objectInstance::class
12 | return String.format("%s{%s}", clazz.simpleName, Integer.toHexString(clazz.hashCode()))
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/JobTrackerApplication.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui
2 |
3 | import android.app.Activity
4 | import android.app.Application
5 | import com.nerdery.rtaza.jobtracker.BuildConfig
6 | import com.nerdery.rtaza.jobtracker.di.component.DaggerApplicationComponent
7 | import dagger.android.AndroidInjector
8 | import dagger.android.DispatchingAndroidInjector
9 | import dagger.android.HasActivityInjector
10 | import io.reactivex.exceptions.CompositeException
11 | import io.reactivex.exceptions.OnErrorNotImplementedException
12 | import io.reactivex.exceptions.UndeliverableException
13 | import io.reactivex.plugins.RxJavaPlugins
14 | import timber.log.Timber
15 | import javax.inject.Inject
16 |
17 | class JobTrackerApplication : Application(), HasActivityInjector {
18 | @Inject lateinit var activityInjector: DispatchingAndroidInjector
19 |
20 | override fun onCreate() {
21 | DaggerApplicationComponent.builder()
22 | .application(this)
23 | .build()
24 | .inject(this)
25 | super.onCreate()
26 |
27 | if (BuildConfig.DEBUG) {
28 | Timber.plant(Timber.DebugTree())
29 | }
30 |
31 | setUncaughtRxJavaErrorHandler()
32 | }
33 |
34 | /**
35 | * Handles and logs all uncaught exceptions thrown through any RxJava stream in the application that does not
36 | * override onError. This prevents the app from crashing if a stream that doesn't override onError receives an
37 | * exception.
38 | */
39 | private fun setUncaughtRxJavaErrorHandler() {
40 | RxJavaPlugins.setErrorHandler { throwable ->
41 | val cause: Throwable? =
42 | if (throwable is OnErrorNotImplementedException || throwable is UndeliverableException) {
43 | throwable.cause
44 | } else {
45 | throwable
46 | }
47 |
48 | if (throwable is CompositeException) {
49 | for (exception in throwable.exceptions) {
50 | Timber.e(exception)
51 | }
52 | } else {
53 | Timber.e(cause)
54 | }
55 | }
56 | }
57 |
58 | override fun activityInjector(): AndroidInjector {
59 | return activityInjector
60 | }
61 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/core/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.core
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 | import androidx.appcompat.widget.Toolbar
6 | import com.nerdery.rtaza.jobtracker.R
7 | import com.nerdery.rtaza.jobtracker.log.LogUtil
8 | import timber.log.Timber
9 |
10 | abstract class BaseActivity : AppCompatActivity() {
11 |
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | Timber.tag(LogUtil.getLogTag(this))
14 | Timber.v("onCreate")
15 | super.onCreate(savedInstanceState)
16 | }
17 |
18 | override fun setContentView(layoutResID: Int) {
19 | super.setContentView(layoutResID)
20 | val toolbar: Toolbar? = findViewById(R.id.actionBar)
21 | if (toolbar != null) {
22 | initializeAppBar(toolbar)
23 | }
24 | }
25 |
26 | private fun initializeAppBar(toolbar: Toolbar) {
27 | setSupportActionBar(toolbar)
28 |
29 | if (parentActivityIntent != null) {
30 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
31 | }
32 | }
33 |
34 | override fun onStart() {
35 | Timber.tag(LogUtil.getLogTag(this))
36 | Timber.v("onStart")
37 | super.onStart()
38 | }
39 |
40 | override fun onResume() {
41 | Timber.tag(LogUtil.getLogTag(this))
42 | Timber.v("onResume")
43 | super.onResume()
44 | }
45 |
46 | override fun onPause() {
47 | Timber.tag(LogUtil.getLogTag(this))
48 | Timber.v("onPause")
49 | super.onPause()
50 | }
51 |
52 | override fun onSaveInstanceState(outState: Bundle) {
53 | Timber.tag(LogUtil.getLogTag(this))
54 | Timber.v("onSaveInstanceState")
55 | super.onSaveInstanceState(outState)
56 | }
57 |
58 | override fun onStop() {
59 | Timber.tag(LogUtil.getLogTag(this))
60 | Timber.v("onStop")
61 | super.onStop()
62 | }
63 |
64 | override fun onDestroy() {
65 | Timber.tag(LogUtil.getLogTag(this))
66 | Timber.v("onDestroy")
67 | super.onDestroy()
68 | }
69 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/core/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.core
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.AndroidViewModel
5 | import io.reactivex.disposables.CompositeDisposable
6 |
7 | abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
8 | val compositeDisposable = CompositeDisposable()
9 |
10 | override fun onCleared() {
11 | compositeDisposable.clear()
12 | super.onCleared()
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/core/SingleLiveEvent.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.core
2 |
3 | import androidx.annotation.MainThread
4 | import androidx.lifecycle.LifecycleOwner
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.Observer
7 | import timber.log.Timber
8 | import java.util.concurrent.atomic.AtomicBoolean
9 |
10 | /**
11 | * A lifecycle-aware observable that sends only new updates after subscription. Used for events like navigation and
12 | * Toast messages.
13 | *
14 | * This avoids a common problem with LiveData: on configuration change (like rotation of the device), an update can
15 | * be emitted if the observer is active. This LiveData only calls the observable if there's an explicit call to
16 | * setValue() or call().
17 | *
18 | * Note that only one observer is going to be notified of changes.
19 | */
20 | class SingleLiveEvent : MutableLiveData() {
21 | private val pending = AtomicBoolean(false)
22 |
23 | @MainThread
24 | override fun observe(owner: LifecycleOwner, observer: Observer) {
25 | if (hasActiveObservers()) {
26 | Timber.w("Multiple observers registered but only one will be notified of changes")
27 | }
28 |
29 | // Observe the internal MutableLiveData
30 | super.observe(owner, Observer { data ->
31 | if (pending.compareAndSet(true, false)) {
32 | observer.onChanged(data)
33 | }
34 | })
35 | }
36 |
37 | @MainThread
38 | override fun setValue(value: DataType?) {
39 | pending.set(true)
40 | super.setValue(value)
41 | }
42 |
43 | /**
44 | * Used for cases where T is Void, to make calls cleaner.
45 | */
46 | @MainThread
47 | fun call() {
48 | value = null
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/jobs/JobViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.jobs
2 |
3 | import android.view.View
4 | import androidx.annotation.ColorInt
5 | import androidx.core.content.ContextCompat
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.nerdery.rtaza.jobtracker.ui.util.*
8 | import kotlinx.android.synthetic.main.item_job.view.*
9 |
10 | class JobViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
11 | private lateinit var presentationModel: JobsViewModel.Presentation.Model
12 |
13 | fun bind(presentationModel: JobsViewModel.Presentation.Model) {
14 | this.presentationModel = presentationModel
15 | itemView.apply {
16 | taskImageView.setImageResource(presentationModel.jobTaskIconId)
17 | addressLine1TextView.text = presentationModel.addressLine1Text
18 | addressLine2TextView.text = presentationModel.addressLine2Text
19 | taskTypeTextView.text = context.getString(presentationModel.jobTaskTextId)
20 | statusTextView.text = context.getString(presentationModel.jobStatusTextId)
21 | workerTextView.text = presentationModel.workerNameText
22 | updateEta()
23 | }
24 | itemView.setOnClickListener {
25 | // TODO: Start detail Activity
26 | }
27 | }
28 |
29 | fun updateEta() {
30 | itemView.apply {
31 | val dueInTime = TimeUtil.calculateDueInTime(presentationModel.eta)
32 | etaTextView.text = TextFormatter.formatDueInTime(itemView.context, dueInTime)
33 | @ColorInt val etaStatusColor = ContextCompat.getColor(
34 | context, JobIconUtil.getEtaStatusColorId(presentationModel.jobStatus, presentationModel.eta)
35 | )
36 | taskImageView.setBackgroundTint(etaStatusColor)
37 |
38 | if (dueInTime < 0) {
39 | etaTextView.setTextColor(etaStatusColor)
40 | } else {
41 | etaTextView.setTextColor(context.resolveThemeAttribute(android.R.attr.textColorPrimary))
42 | }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/jobs/JobsActivity.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.jobs
2 |
3 | import android.os.Bundle
4 | import android.view.View
5 | import androidx.lifecycle.ViewModelProvider
6 | import androidx.lifecycle.ViewModelProviders
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import com.nerdery.rtaza.jobtracker.R
9 | import com.nerdery.rtaza.jobtracker.ui.core.BaseActivity
10 | import com.nerdery.rtaza.jobtracker.ui.util.observeNonNull
11 | import com.nerdery.rtaza.jobtracker.ui.view.OffsetItemDecoration
12 | import dagger.android.AndroidInjection
13 | import kotlinx.android.synthetic.main.activity_jobs.*
14 | import javax.inject.Inject
15 |
16 | class JobsActivity : BaseActivity() {
17 | private lateinit var viewModel: JobsViewModel
18 | @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
19 | @Inject lateinit var listAdapter: JobsListAdapter
20 |
21 | override fun onCreate(savedInstanceState: Bundle?) {
22 | AndroidInjection.inject(this)
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_jobs)
25 |
26 | recyclerView.apply {
27 | addItemDecoration(
28 | OffsetItemDecoration(
29 | topOffset = resources.getDimensionPixelSize(R.dimen.spacing_small),
30 | bottomOffset = resources.getDimensionPixelSize(R.dimen.spacing_small),
31 | leftOffset = resources.getDimensionPixelSize(R.dimen.spacing_medium),
32 | rightOffset = resources.getDimensionPixelSize(R.dimen.spacing_medium)
33 | )
34 | )
35 | lifecycle.addObserver(listAdapter)
36 | adapter = listAdapter
37 | layoutManager = LinearLayoutManager(context)
38 | }
39 |
40 | viewModel = ViewModelProviders.of(this, viewModelFactory).get(JobsViewModel::class.java)
41 |
42 | viewModel.loading.observeNonNull(this) { loading ->
43 | progressBar.visibility = if (loading && listAdapter.isEmpty()) {
44 | View.VISIBLE
45 | } else {
46 | View.GONE
47 | }
48 | }
49 |
50 | viewModel.getPresentation().observeNonNull(this) { presentation ->
51 | toggleEmptyStateVisibility(false)
52 | listAdapter.submitList(presentation.models)
53 | }
54 |
55 | viewModel.getNoJobsFound().observeNonNull(this) {
56 | emptyStateImageView.setImageResource(R.drawable.icon_no_active_jobs)
57 | emptyStateTitleTextView.setText(R.string.empty_state_title_no_jobs)
58 | emptyStateDescriptionTextView.setText(R.string.empty_state_description_no_jobs)
59 | toggleEmptyStateVisibility(true)
60 | }
61 |
62 | viewModel.error.observeNonNull(this) { error ->
63 | if (listAdapter.isEmpty()) {
64 | emptyStateImageView.setImageResource(error.iconResourceId)
65 | emptyStateTitleTextView.setText(error.titleResourceId)
66 | emptyStateDescriptionTextView.setText(error.descriptionResourceId)
67 | toggleEmptyStateVisibility(true)
68 | }
69 | // Don't show an error if there're jobs in the list loaded from local persistence since the request was not
70 | // explicitly requested by the user, but consider adding some sort of indication that the jobs being shown
71 | // may not be the freshest compared to what's available on the backend.
72 | }
73 |
74 | viewModel.bind()
75 | }
76 |
77 | private fun toggleEmptyStateVisibility(show: Boolean) {
78 | if (show) {
79 | recyclerView.visibility = View.INVISIBLE
80 | emptyStateGroup.visibility = View.VISIBLE
81 | } else {
82 | emptyStateGroup.visibility = View.GONE
83 | recyclerView.visibility = View.VISIBLE
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/jobs/JobsListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.jobs
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.lifecycle.Lifecycle
6 | import androidx.lifecycle.LifecycleObserver
7 | import androidx.lifecycle.OnLifecycleEvent
8 | import androidx.recyclerview.widget.DiffUtil
9 | import androidx.recyclerview.widget.ListAdapter
10 | import com.nerdery.rtaza.jobtracker.R
11 | import com.nerdery.rtaza.jobtracker.ui.util.isNullOrEmpty
12 | import io.reactivex.Observable
13 | import io.reactivex.android.schedulers.AndroidSchedulers
14 | import io.reactivex.disposables.Disposable
15 | import java.util.concurrent.TimeUnit
16 |
17 | class JobsListAdapter : ListAdapter(diffUtilItemCallback),
18 | LifecycleObserver {
19 | private var dataSet: List? = null
20 | private val etaUpdateInterval = Pair(1, TimeUnit.MINUTES)
21 | private var etaUpdateObservable: Observable
22 | private var etaUpdateDisposable: Disposable? = null
23 |
24 | init {
25 | setHasStableIds(true)
26 | etaUpdateObservable = Observable.interval(etaUpdateInterval.first, etaUpdateInterval.second)
27 | .observeOn(AndroidSchedulers.mainThread())
28 | }
29 |
30 | override fun submitList(list: List?) {
31 | // Reset the ETA update observable anytime a new list is submitted
32 | etaUpdateDisposable?.dispose()
33 | dataSet = list
34 | super.submitList(list)
35 | startEtaUpdateInterval()
36 | }
37 |
38 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): JobViewHolder {
39 | val itemView = LayoutInflater.from(parent.context).inflate(R.layout.item_job, parent, false)
40 | return JobViewHolder(itemView)
41 | }
42 |
43 | override fun onBindViewHolder(holder: JobViewHolder, position: Int) {
44 | holder.bind(getItem(position))
45 | }
46 |
47 | override fun onBindViewHolder(holder: JobViewHolder, position: Int, payloads: MutableList) {
48 | if (payloads.isEmpty()) {
49 | onBindViewHolder(holder, position)
50 | } else {
51 | for (payload in payloads) {
52 | when (payload) {
53 | is EtaUpdatePayload -> holder.updateEta()
54 | }
55 | }
56 | }
57 | }
58 |
59 | override fun getItemId(position: Int): Long {
60 | return getItem(position).jobId
61 | }
62 |
63 | fun isEmpty() = dataSet.isNullOrEmpty()
64 |
65 | @OnLifecycleEvent(Lifecycle.Event.ON_START)
66 | fun startEtaUpdateInterval() {
67 | if (!isEmpty()) {
68 | etaUpdateDisposable = etaUpdateObservable.subscribe {
69 | updateEtas()
70 | }
71 | }
72 | }
73 |
74 | @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
75 | fun clearDisposables() {
76 | etaUpdateDisposable?.dispose()
77 | }
78 |
79 | private fun updateEtas() {
80 | notifyItemRangeChanged(0, itemCount, EtaUpdatePayload())
81 | }
82 | }
83 |
84 | private val diffUtilItemCallback = object : DiffUtil.ItemCallback() {
85 | override fun areItemsTheSame(
86 | oldModel: JobsViewModel.Presentation.Model,
87 | newModel: JobsViewModel.Presentation.Model
88 | ): Boolean {
89 | return oldModel.jobId == newModel.jobId
90 | }
91 |
92 | override fun areContentsTheSame(
93 | oldModel: JobsViewModel.Presentation.Model,
94 | newModel: JobsViewModel.Presentation.Model
95 | ): Boolean {
96 | return oldModel == newModel
97 | }
98 |
99 | override fun getChangePayload(
100 | oldItem: JobsViewModel.Presentation.Model,
101 | newItem: JobsViewModel.Presentation.Model
102 | ): Any {
103 | // Prevent recyclable view holders from being recreated and trigger a full rebind
104 | return newItem
105 | }
106 | }
107 |
108 | private class EtaUpdatePayload
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/jobs/JobsViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.jobs
2 |
3 | import android.app.Application
4 | import android.content.Context
5 | import androidx.annotation.DrawableRes
6 | import androidx.annotation.StringRes
7 | import androidx.lifecycle.LiveData
8 | import androidx.lifecycle.MutableLiveData
9 | import com.nerdery.rtaza.jobtracker.R
10 | import com.nerdery.rtaza.jobtracker.data.core.Error
11 | import com.nerdery.rtaza.jobtracker.data.core.Resource
12 | import com.nerdery.rtaza.jobtracker.data.local.model.JobStatus
13 | import com.nerdery.rtaza.jobtracker.data.local.model.JobWithRelations
14 | import com.nerdery.rtaza.jobtracker.data.repository.JobRepository
15 | import com.nerdery.rtaza.jobtracker.ui.core.BaseViewModel
16 | import com.nerdery.rtaza.jobtracker.ui.core.SingleLiveEvent
17 | import com.nerdery.rtaza.jobtracker.ui.util.JobIconUtil
18 | import com.nerdery.rtaza.jobtracker.ui.util.TextFormatter
19 | import io.reactivex.rxkotlin.addTo
20 | import io.reactivex.schedulers.Schedulers
21 | import javax.inject.Inject
22 |
23 | class JobsViewModel @Inject constructor(
24 | application: Application,
25 | private val jobRepository: JobRepository
26 | ) : BaseViewModel(application) {
27 | val loading: SingleLiveEvent = SingleLiveEvent()
28 | val error: SingleLiveEvent = SingleLiveEvent()
29 | private val presentation: MutableLiveData = MutableLiveData()
30 | private val noJobsFound: MutableLiveData = MutableLiveData()
31 |
32 | fun getPresentation(): LiveData = presentation
33 |
34 | fun getNoJobsFound(): LiveData = noJobsFound
35 |
36 | fun bind() {
37 | jobRepository.getJobs(true)
38 | .subscribeOn(Schedulers.io())
39 | .subscribe { resource: Resource> ->
40 | when (resource) {
41 | is Resource.Loading -> {
42 | if (resource.data != null) {
43 | presentation.postValue(Presentation(getApplication(), resource.data))
44 | }
45 | loading.postValue(true)
46 | }
47 | is Resource.ResourceFound -> {
48 | presentation.postValue(Presentation(getApplication(), resource.data!!))
49 | loading.postValue(false)
50 | }
51 | is Resource.NoResourceFound -> {
52 | noJobsFound.postValue(Unit)
53 | loading.postValue(false)
54 | }
55 | is Resource.Error -> {
56 | error.postValue(resource.error)
57 | loading.postValue(false)
58 | }
59 | }
60 | }.addTo(compositeDisposable)
61 | }
62 |
63 | data class Presentation(
64 | private val context: Context,
65 | private val jobsWithRelations: List
66 | ) {
67 | val models: MutableList = ArrayList(jobsWithRelations.size)
68 |
69 | init {
70 | jobsWithRelations.forEach { jobWithRelations ->
71 | with(jobWithRelations.job) {
72 | models.add(
73 | Model(
74 | id,
75 | getStatus(),
76 | getStatus().label,
77 | task.label,
78 | JobIconUtil.getTaskIconId(task),
79 | location.street,
80 | context.getString(R.string.format_city_state, location.city, location.state),
81 | TextFormatter.formatFullName(
82 | context,
83 | jobWithRelations.worker?.firstName,
84 | jobWithRelations.worker?.lastName
85 | ),
86 | eta
87 | )
88 | )
89 | }
90 | }
91 | }
92 |
93 | data class Model(
94 | val jobId: Long,
95 | val jobStatus: JobStatus,
96 | @StringRes val jobStatusTextId: Int,
97 | @StringRes val jobTaskTextId: Int,
98 | @DrawableRes val jobTaskIconId: Int,
99 | val addressLine1Text: String,
100 | val addressLine2Text: String,
101 | val workerNameText: String,
102 | val eta: Long
103 | )
104 | }
105 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/CollectionExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | fun Collection?.isNullOrEmpty(): Boolean {
4 | return this == null || isEmpty()
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/ContextExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import android.content.Context
4 | import android.util.TypedValue
5 | import androidx.annotation.AttrRes
6 |
7 | /**
8 | * Retrieve the value of an attribute in the Theme.
9 | */
10 | fun Context.resolveThemeAttribute(@AttrRes resourceId: Int): Int {
11 | val typedValue = TypedValue()
12 | theme.resolveAttribute(resourceId, typedValue, true)
13 | return typedValue.data
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/JobIconUtil.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import androidx.annotation.ColorRes
4 | import androidx.annotation.DrawableRes
5 | import com.nerdery.rtaza.jobtracker.R
6 | import com.nerdery.rtaza.jobtracker.data.local.model.JobStatus
7 | import com.nerdery.rtaza.jobtracker.data.local.model.JobTask
8 |
9 | const val JUDICIOUSLY_LATE_IN_MINUTES = 1
10 | const val EXCEEDINGLY_LATE_IN_MINUTES = 11
11 |
12 | class JobIconUtil {
13 |
14 | companion object {
15 |
16 | @DrawableRes
17 | fun getTaskIconId(task: JobTask): Int {
18 | return when (task) {
19 | JobTask.ELECTRICAL -> R.drawable.icon_electrical
20 | JobTask.HVAC -> R.drawable.icon_hvac
21 | JobTask.PLUMBING -> R.drawable.icon_plumbing
22 | JobTask.CARPENTRY -> R.drawable.icon_carpentry
23 | JobTask.PAINT -> R.drawable.icon_paint
24 | }
25 | }
26 |
27 | @ColorRes
28 | fun getEtaStatusColorId(status: JobStatus, promisedEta: Long?): Int {
29 | if (status === JobStatus.NEW || status === JobStatus.ON_SITE) {
30 | return R.color.job_eta_inapplicable
31 | }
32 |
33 | if (promisedEta == null) {
34 | return R.color.job_eta_unknown
35 | }
36 |
37 | val minutesLate = TimeUtil.calculateMinutesLate(promisedEta)
38 | return when {
39 | minutesLate < JUDICIOUSLY_LATE_IN_MINUTES -> R.color.job_eta_on_time
40 | minutesLate <= EXCEEDINGLY_LATE_IN_MINUTES -> R.color.job_eta_late
41 | else -> R.color.job_eta_exceedingly_late
42 | }
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/LiveDataExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import androidx.lifecycle.LifecycleOwner
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.Observer
6 |
7 | fun LiveData.observeNonNull(owner: LifecycleOwner, observer: (data: T) -> Unit) {
8 | this.observe(owner, Observer { data ->
9 | data?.let(observer)
10 | })
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/StringExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | fun String.Companion.join(delimiter: String?, vararg strings: String?): String? {
4 | val builder = StringBuilder()
5 | var string: String?
6 |
7 | for (index in 0 until strings.size) {
8 | string = strings[index]
9 |
10 | if (!string.isNullOrEmpty()) {
11 | if (builder.isNotEmpty()) {
12 | builder.append(delimiter)
13 | }
14 | builder.append(string)
15 | }
16 | }
17 |
18 | return if (builder.isEmpty()) null else builder.toString()
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/TextFormatter.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import android.content.Context
4 | import com.nerdery.rtaza.jobtracker.R
5 |
6 | class TextFormatter {
7 |
8 | companion object {
9 |
10 | fun formatFullName(context: Context, firstName: String?, lastName: String?): String {
11 | val joinedStrings = String.join(" ", firstName, lastName)
12 |
13 | return joinedStrings ?: context.getString(R.string.data_not_available)
14 | }
15 |
16 | fun formatDueInTime(context: Context, dueInTime: Int): String {
17 | return when {
18 | dueInTime > 0 -> context.resources.getQuantityString(R.plurals.minutes, dueInTime, dueInTime)
19 | dueInTime < 0 -> context.getString(R.string.eta_negative)
20 | else -> context.getString(R.string.eta_now)
21 | }
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/TimeUtil.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import java.util.concurrent.TimeUnit
4 |
5 | class TimeUtil {
6 |
7 | companion object {
8 |
9 | fun getCurrentTimeMillis() = System.currentTimeMillis()
10 |
11 | /**
12 | * Calculates the difference between the [promisedEta] and the current time.
13 | *
14 | * @param promisedEta the promised ETA in milliseconds.
15 | * @return the difference between the [promisedEta] and the current time in minutes.
16 | */
17 | fun calculateDueInTime(promisedEta: Long): Int {
18 | return (TimeUnit.MILLISECONDS.toMinutes(promisedEta) -
19 | TimeUnit.MILLISECONDS.toMinutes(getCurrentTimeMillis())).toInt()
20 | }
21 |
22 | /**
23 | * Calculates the difference between the the current time and the [promisedEta].
24 | *
25 | * @param promisedEta the promised ETA in milliseconds.
26 | * @return the difference between the current time and the [promisedEta] in minutes.
27 | * Returns 0 if the difference is negative (not late).
28 | */
29 | fun calculateMinutesLate(promisedEta: Long): Int {
30 | val diff = TimeUnit.MILLISECONDS.toMinutes(getCurrentTimeMillis()) -
31 | TimeUnit.MILLISECONDS.toMinutes(promisedEta)
32 | return if (diff > 0) diff.toInt() else 0
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/ViewExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import android.content.res.ColorStateList
4 | import android.view.View
5 | import androidx.annotation.ColorInt
6 | import androidx.core.view.ViewCompat
7 |
8 | fun View.setBackgroundTint(@ColorInt color: Int) =
9 | ViewCompat.setBackgroundTintList(this, ColorStateList.valueOf(color))
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/util/Visibility.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.util
2 |
3 | import android.view.View
4 | import androidx.annotation.IntDef
5 |
6 | @IntDef(View.VISIBLE, View.INVISIBLE, View.GONE)
7 | @Retention(AnnotationRetention.SOURCE)
8 | annotation class Visibility
--------------------------------------------------------------------------------
/app/src/main/java/com/nerdery/rtaza/jobtracker/ui/view/OffsetItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.nerdery.rtaza.jobtracker.ui.view
2 |
3 | import android.graphics.Rect
4 | import android.view.View
5 | import androidx.recyclerview.widget.RecyclerView
6 |
7 | /**
8 | * [RecyclerView.ItemDecoration] responsible for adding top, bottom, left, and right offsets to each item view of a
9 | * [RecyclerView].
10 | */
11 | class OffsetItemDecoration(
12 | private val topOffset: Int = 0,
13 | private val bottomOffset: Int = 0,
14 | private val leftOffset: Int = 0,
15 | private val rightOffset: Int = 0
16 | ) : RecyclerView.ItemDecoration() {
17 |
18 | override fun getItemOffsets(
19 | offsetsRect: Rect,
20 | itemView: View,
21 | recyclerView: RecyclerView,
22 | state: RecyclerView.State
23 | ) {
24 | with(offsetsRect) {
25 | if (recyclerView.getChildAdapterPosition(itemView) == 0) {
26 | top = topOffset
27 | }
28 | bottom = bottomOffset
29 | left = leftOffset
30 | right = rightOffset
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/foreground_launch.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/background_launch.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_background_job_status.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_carpentry.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_electrical.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_error_client_io.xml:
--------------------------------------------------------------------------------
1 |
7 |
11 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_error_server.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
13 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_error_uauthorized.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_error_unknown.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_hvac.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_no_active_jobs.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_paint.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/icon_plumbing.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_jobs.xml:
--------------------------------------------------------------------------------
1 |
2 |
10 |
11 |
14 |
15 |
25 |
26 |
34 |
35 |
40 |
41 |
52 |
53 |
67 |
68 |
82 |
83 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/item_job.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
16 |
17 |
28 |
29 |
42 |
43 |
54 |
55 |
62 |
63 |
75 |
76 |
87 |
88 |
100 |
101 |
112 |
113 |
119 |
120 |
130 |
131 |
141 |
142 |
152 |
153 |
163 |
164 |
165 |
166 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/icon_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/icon_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/icon_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-hdpi/icon_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/icon_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-hdpi/icon_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/icon_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-mdpi/icon_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/icon_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-mdpi/icon_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xhdpi/icon_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/icon_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xhdpi/icon_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/icon_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xxhdpi/icon_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/icon_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xxhdpi/icon_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/icon_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xxxhdpi/icon_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/icon_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/app/src/main/res/mipmap-xxxhdpi/icon_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | #039be5
6 | #01579b
7 | #ff4081
8 | #ffffff
9 | #e5e5e5
10 |
11 |
12 | #de000000
13 | #8a000000
14 | #61000000
15 | #33000000
16 |
17 |
18 | #ffffff
19 | #b2ffffff
20 | #80ffffff
21 | #1fffffff
22 |
23 |
24 | #ffffff
25 | #0d000000
26 | #1f000000
27 |
28 |
29 | #039be5
30 | #43a047
31 | #fB8c00
32 | #e53935
33 | #000000
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | 10sp
6 | 12sp
7 | 14sp
8 | 14sp
9 | 16sp
10 | 20sp
11 | 24sp
12 | 34sp
13 | 48sp
14 |
15 |
16 | 4dp
17 | 8dp
18 | 12dp
19 | 16dp
20 | 24dp
21 | 32dp
22 | 48dp
23 |
24 |
25 | 24dp
26 | 48dp
27 | 60dp
28 | 105dp
29 | 130dp
30 |
31 |
32 | 18dp
33 | 24dp
34 | 36dp
35 | 48dp
36 |
37 |
38 | 4dp
39 | 2dp
40 | 1dp
41 |
42 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Job Tracker
5 | Request failed
6 | Check your network connection
7 | Request failed
8 | Something went wrong on our end
9 | Session expired
10 | Please log in again
11 | Request failed
12 | Something went wrong
13 | %1$s. %2$s.
14 | --
15 |
16 |
17 | New
18 | Pending
19 | Declined
20 | Missed
21 | Accepted
22 | Rejected
23 | En Route
24 | On Site
25 | Completed
26 | Cancelled
27 | Dispatcher Cancelled
28 |
29 |
30 | Carpentry
31 | Paint
32 | Electrical
33 | Plumbing
34 | HVAC
35 |
36 |
37 |
38 | - %d minute
39 | - %d minutes
40 |
41 | Due now
42 | Overdue
43 | Task type
44 | Status
45 | Worker
46 | ETA
47 | %1$s, %2$s
48 |
49 |
50 | Active jobs
51 | No active jobs found
52 | We\'re looking for more work for you. In the meantime, take a break, free your mind, and relax your body.
53 |
54 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
11 |
12 |
15 |
16 |
17 |
18 |
24 |
25 |
31 |
32 |
40 |
41 |
50 |
51 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
16 |
17 |
20 |
21 |
24 |
25 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | ext {
3 | kotlinVersion = "1.3.20"
4 | appCompatVersion = "1.1.0-alpha01"
5 | lifecycleVersion = "2.1.0-alpha02"
6 | materialVersion = "1.0.0"
7 | recyclerViewVersion = "1.1.0-alpha02"
8 | cardViewVersion = "1.0.0"
9 | constraintLayoutVersion = "1.1.3"
10 | roomVersion = "2.1.0-alpha04"
11 | retrofitVersion = "2.4.0"
12 | okHttpVersion = "3.10.0"
13 | rxAndroidVersion = "2.1.0"
14 | rxKotlinVersion = "2.3.0"
15 | moshiVersion = "1.6.0"
16 | daggerVersion = "2.16"
17 | timberVersion = "4.7.1"
18 | }
19 |
20 | repositories {
21 | google()
22 | jcenter()
23 | }
24 |
25 | dependencies {
26 | classpath "com.android.tools.build:gradle:3.3.0"
27 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion"
28 | }
29 | }
30 |
31 | allprojects {
32 | repositories {
33 | google()
34 | jcenter()
35 | }
36 | }
37 |
38 | task clean(type: Delete) {
39 | delete rootProject.buildDir
40 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | android.enableJetifier=true
10 | android.useAndroidX=true
11 | org.gradle.jvmargs=-Xmx1536m
12 | # When configured, Gradle will run in incubating parallel mode.
13 | # This option should only be used with decoupled projects. More details, visit
14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
15 | # org.gradle.parallel=true
16 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ryandt/JobTracker/926ca3679231eb4d72aba9d9b6350c2e504daacc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Jan 30 16:56:44 CST 2019
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/schemas/com.nerdery.rtaza.jobtracker.data.local.ApplicationDatabase/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "formatVersion": 1,
3 | "database": {
4 | "version": 3,
5 | "identityHash": "7f615c3142a38943614a602fcb6fe12d",
6 | "entities": [
7 | {
8 | "tableName": "Job",
9 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `task` INTEGER NOT NULL, `status` INTEGER, `active` INTEGER NOT NULL, `workerId` INTEGER, `eta` INTEGER NOT NULL, `notes` TEXT, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `phoneNumber` TEXT NOT NULL, `street` TEXT NOT NULL, `city` TEXT NOT NULL, `zip` TEXT NOT NULL, `state` TEXT NOT NULL, `latitude` REAL NOT NULL, `longitude` REAL NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`workerId`) REFERENCES `Worker`(`id`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
10 | "fields": [
11 | {
12 | "fieldPath": "id",
13 | "columnName": "id",
14 | "affinity": "INTEGER",
15 | "notNull": true
16 | },
17 | {
18 | "fieldPath": "task",
19 | "columnName": "task",
20 | "affinity": "INTEGER",
21 | "notNull": true
22 | },
23 | {
24 | "fieldPath": "status",
25 | "columnName": "status",
26 | "affinity": "INTEGER",
27 | "notNull": false
28 | },
29 | {
30 | "fieldPath": "active",
31 | "columnName": "active",
32 | "affinity": "INTEGER",
33 | "notNull": true
34 | },
35 | {
36 | "fieldPath": "workerId",
37 | "columnName": "workerId",
38 | "affinity": "INTEGER",
39 | "notNull": false
40 | },
41 | {
42 | "fieldPath": "eta",
43 | "columnName": "eta",
44 | "affinity": "INTEGER",
45 | "notNull": true
46 | },
47 | {
48 | "fieldPath": "notes",
49 | "columnName": "notes",
50 | "affinity": "TEXT",
51 | "notNull": false
52 | },
53 | {
54 | "fieldPath": "customer.firstName",
55 | "columnName": "firstName",
56 | "affinity": "TEXT",
57 | "notNull": true
58 | },
59 | {
60 | "fieldPath": "customer.lastName",
61 | "columnName": "lastName",
62 | "affinity": "TEXT",
63 | "notNull": true
64 | },
65 | {
66 | "fieldPath": "customer.phoneNumber",
67 | "columnName": "phoneNumber",
68 | "affinity": "TEXT",
69 | "notNull": true
70 | },
71 | {
72 | "fieldPath": "location.street",
73 | "columnName": "street",
74 | "affinity": "TEXT",
75 | "notNull": true
76 | },
77 | {
78 | "fieldPath": "location.city",
79 | "columnName": "city",
80 | "affinity": "TEXT",
81 | "notNull": true
82 | },
83 | {
84 | "fieldPath": "location.zip",
85 | "columnName": "zip",
86 | "affinity": "TEXT",
87 | "notNull": true
88 | },
89 | {
90 | "fieldPath": "location.state",
91 | "columnName": "state",
92 | "affinity": "TEXT",
93 | "notNull": true
94 | },
95 | {
96 | "fieldPath": "location.latitude",
97 | "columnName": "latitude",
98 | "affinity": "REAL",
99 | "notNull": true
100 | },
101 | {
102 | "fieldPath": "location.longitude",
103 | "columnName": "longitude",
104 | "affinity": "REAL",
105 | "notNull": true
106 | }
107 | ],
108 | "primaryKey": {
109 | "columnNames": [
110 | "id"
111 | ],
112 | "autoGenerate": false
113 | },
114 | "indices": [
115 | {
116 | "name": "index_Job_workerId",
117 | "unique": false,
118 | "columnNames": [
119 | "workerId"
120 | ],
121 | "createSql": "CREATE INDEX `index_Job_workerId` ON `${TABLE_NAME}` (`workerId`)"
122 | }
123 | ],
124 | "foreignKeys": [
125 | {
126 | "table": "Worker",
127 | "onDelete": "NO ACTION",
128 | "onUpdate": "NO ACTION",
129 | "columns": [
130 | "workerId"
131 | ],
132 | "referencedColumns": [
133 | "id"
134 | ]
135 | }
136 | ]
137 | },
138 | {
139 | "tableName": "Worker",
140 | "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `online` INTEGER NOT NULL, PRIMARY KEY(`id`))",
141 | "fields": [
142 | {
143 | "fieldPath": "id",
144 | "columnName": "id",
145 | "affinity": "INTEGER",
146 | "notNull": true
147 | },
148 | {
149 | "fieldPath": "firstName",
150 | "columnName": "firstName",
151 | "affinity": "TEXT",
152 | "notNull": true
153 | },
154 | {
155 | "fieldPath": "lastName",
156 | "columnName": "lastName",
157 | "affinity": "TEXT",
158 | "notNull": true
159 | },
160 | {
161 | "fieldPath": "online",
162 | "columnName": "online",
163 | "affinity": "INTEGER",
164 | "notNull": true
165 | }
166 | ],
167 | "primaryKey": {
168 | "columnNames": [
169 | "id"
170 | ],
171 | "autoGenerate": false
172 | },
173 | "indices": [],
174 | "foreignKeys": []
175 | }
176 | ],
177 | "setupQueries": [
178 | "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
179 | "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"7f615c3142a38943614a602fcb6fe12d\")"
180 | ]
181 | }
182 | }
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------