├── .github
└── workflows
│ └── pre-workflow.yml
├── .gitignore
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── encodings.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── misc.xml
├── runConfigurations.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── gts
│ │ └── trackmypath
│ │ └── MainActivityTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── gts
│ │ │ └── trackmypath
│ │ │ ├── AppModule.kt
│ │ │ ├── TrackMyPathApplication.kt
│ │ │ ├── common
│ │ │ └── Result.kt
│ │ │ ├── data
│ │ │ ├── PhotoRepositoryImpl.kt
│ │ │ ├── database
│ │ │ │ ├── PhotoDao.kt
│ │ │ │ ├── PhotoDatabase.kt
│ │ │ │ └── PhotoEntity.kt
│ │ │ └── network
│ │ │ │ ├── FlickrApi.kt
│ │ │ │ ├── FlickrClient.kt
│ │ │ │ ├── FlickrClientImpl.kt
│ │ │ │ └── ResponseEntity.kt
│ │ │ ├── domain
│ │ │ ├── LocationServiceInteractor.kt
│ │ │ ├── PhotoRepository.kt
│ │ │ ├── model
│ │ │ │ └── Photo.kt
│ │ │ └── usecase
│ │ │ │ ├── ClearPhotosUseCase.kt
│ │ │ │ ├── RetrievePhotosUseCase.kt
│ │ │ │ └── SearchPhotoByLocationUseCase.kt
│ │ │ └── presentation
│ │ │ ├── MainActivity.kt
│ │ │ ├── PhotoAdapter.kt
│ │ │ ├── PhotoStreamFragment.kt
│ │ │ ├── PhotoStreamViewModel.kt
│ │ │ ├── Utils.kt
│ │ │ ├── model
│ │ │ └── PhotoViewItem.kt
│ │ │ └── service
│ │ │ └── LocationService.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ └── ic_launcher_background.xml
│ │ ├── layout
│ │ ├── activity_main.xml
│ │ ├── fragment_photo_stream.xml
│ │ └── photo_item.xml
│ │ ├── mipmap-anydpi-v26
│ │ ├── ic_launcher.xml
│ │ └── ic_launcher_round.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ └── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── styles.xml
│ └── test
│ └── java
│ └── com
│ └── gts
│ └── trackmypath
│ ├── data
│ └── repository
│ │ └── PhotoRepositoryImplTest.kt
│ ├── domain
│ ├── RetrievePhotosUseCaseTest.kt
│ └── SearchPhotoByLocationUseCaseTest.kt
│ └── presentation
│ ├── LiveDataTestUtil.kt
│ ├── PhotoStreamViewModelTest.kt
│ └── UtilsTest.kt
├── build.gradle
├── config
└── detekt
│ └── detekt.yml
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── screenshots
├── scrn1.png
└── summary.png
└── settings.gradle
/.github/workflows/pre-workflow.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | # Controls when the action will run.
6 | on:
7 | # Triggers the workflow on push or pull request events but only for the master branch
8 | push:
9 | branches: [ master ]
10 | pull_request:
11 | branches:
12 | - '*'
13 |
14 | # Allows you to run this workflow manually from the Actions tab
15 | workflow_dispatch:
16 |
17 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
18 | jobs:
19 | # This workflow contains a single job called "build"
20 | build:
21 | # The type of runner that the job will run on
22 | runs-on: ubuntu-latest
23 |
24 | steps:
25 | - name: Clone Repo
26 | uses: actions/checkout@v2
27 |
28 | - name: set up JDK 1.8
29 | uses: actions/setup-java@v1
30 | with:
31 | java-version: 1.8
32 |
33 | # Execute Android Lint
34 | - name: Android Lint
35 | if: ${{ github.event_name == 'pull_request'}}
36 | run: ./gradlew lintDebug
37 |
38 | # Execute unit tests
39 | - name: Unit Test
40 | run: ./gradlew testDebugUnitTest
41 |
42 | # - name: Android Test Report
43 | # uses: gs-ts/android-test-report-action@v1.2.0
44 | # if: ${{ always() }} # IMPORTANT: run Android Test Report regardless
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | xmlns:android
30 |
31 | ^$
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | xmlns:.*
41 |
42 | ^$
43 |
44 |
45 | BY_NAME
46 |
47 |
48 |
49 |
50 |
51 |
52 | .*:id
53 |
54 | http://schemas.android.com/apk/res/android
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | .*:name
64 |
65 | http://schemas.android.com/apk/res/android
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | name
75 |
76 | ^$
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | style
86 |
87 | ^$
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | ^$
99 |
100 |
101 | BY_NAME
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | http://schemas.android.com/apk/res/android
111 |
112 |
113 | ANDROID_ATTRIBUTE_ORDER
114 |
115 |
116 |
117 |
118 |
119 |
120 | .*
121 |
122 | .*
123 |
124 |
125 | BY_NAME
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
21 |
22 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.idea/runConfigurations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Track my path
4 |
5 | An android app that tracks your walk with images every 100 meters:
6 | - images fetched from Flickr based on location
7 | - pictures are shown in a list, and user can scroll through the stream
8 | - one button start/stop, on each start the previous stream of photos gets wiped
9 | - when the app is removed from background and user has not stopped the tracking, the tracking continues in a service
10 |
11 | *Please create a Flickr account and use your own api key. Add it in the FlickrApi file.*
12 |
13 | ---
14 |
15 | ### MVVM pattern with Clean architecture developed with Kotlin.
16 | Clean architecture consists of three layers:
17 | - **Data**, includes data objects, databases, network clients, repositories.
18 | - **Domain**, includes use cases of business logic. This layer orchestrates the flow of data from Data Layer to Presentation and the other way.
19 | - **Presentation**, includes UI related components, such as ViewModels, Fragments, Activities.
20 |
21 | ##### Android Jetpack Components used:
22 | - Fragment
23 | - ViewModel
24 | - [LifecycleService](https://developer.android.com/reference/androidx/lifecycle/LifecycleService)
25 | - View Binding
26 | - LiveData
27 | - Room
28 | - [Location](https://github.com/googlesamples/android-play-location/tree/master/LocationUpdatesForegroundService)
29 | - [ActivityScenario](https://developer.android.com/guide/components/activities/testing), instrumentation testing (part of AndroidX Test)
30 | - Espresso (UI tests)
31 |
32 | ##### Libraries:
33 | - [Koin](https://insert-koin.io/), (in master branch) an easy-to-use DI framework. [Nice comparison with Dagger](https://medium.com/@farshidabazari/android-koin-with-mvvm-and-retrofit-e040e4e15f9d)
34 | - [Hilt](https://developer.android.com/training/dependency-injection/hilt-android) (in [feature-hilt-di branch](https://github.com/gs-ts/TrackMyPath/tree/feature-hilt-di)) a DI library for Android based on Dagger
35 | - [Kotlin Coroutines](https://developer.android.com/kotlin/coroutines)
36 | - [fresco](https://github.com/facebook/fresco), an Android library for managing images and the memory they use
37 | - [Retrofit](https://square.github.io/retrofit/)
38 | - [OkHttp](https://square.github.io/okhttp/)
39 | - [moshi](https://github.com/square/moshi), JSON library for Kotlin and Java
40 | - [Timber](https://github.com/JakeWharton/timber), a logger which provides utility on top of Android’s Log class
41 | - [detekt](https://github.com/detekt/detekt), Static code analysis for Kotlin
42 |
43 | ##### Flickr API:
44 | - [flickr.photos.search](https://www.flickr.com/services/api/flickr.photos.search.html)
45 |
46 | Sources:
47 | - [Google I/O 2018 app — Architecture and Testing](https://medium.com/androiddevelopers/google-i-o-2018-app-architecture-and-testing-f546e37fc7eb)
48 | - [Clean Architecture of Android Apps with Practical Examples](https://rubygarage.org/blog/clean-android-architecture)
49 | - [Clean Architecture Guide (with tested examples): Data Flow != Dependency Rule](https://proandroiddev.com/clean-architecture-data-flow-dependency-rule-615ffdd79e29)
50 |
51 | ----
52 |
53 | ### Screenshots
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 | apply plugin: 'kotlin-kapt'
4 | apply plugin: 'kotlin-parcelize'
5 | apply plugin: 'io.gitlab.arturbosch.detekt'
6 |
7 | android {
8 | compileSdkVersion 30
9 |
10 | defaultConfig {
11 | applicationId "com.gts.trackmypath"
12 | minSdkVersion 23
13 | targetSdkVersion 30
14 | versionCode 1
15 | versionName "1.0"
16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 |
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 |
31 | buildFeatures {
32 | viewBinding = true
33 | }
34 |
35 | }
36 |
37 | dependencies {
38 | implementation fileTree(dir: 'libs', include: ['*.jar'])
39 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
40 |
41 | implementation "androidx.appcompat:appcompat:$appcompat_version"
42 | implementation "androidx.core:core-ktx:$ktx_version"
43 | implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
44 | implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
45 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
46 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
47 | implementation "androidx.constraintlayout:constraintlayout:$constraintLayout_version"
48 | implementation "androidx.localbroadcastmanager:localbroadcastmanager:$localBroadcastManager_version"
49 | implementation "androidx.preference:preference-ktx:$preference_ktx_version"
50 | implementation "androidx.room:room-ktx:$room_version"
51 | implementation "androidx.room:room-runtime:$room_version"
52 | kapt "androidx.room:room-compiler:$room_version"
53 | implementation "com.facebook.fresco:fresco:$fresco_version"
54 |
55 | implementation "com.google.android.material:material:$material_version"
56 | implementation "com.google.android.gms:play-services-location:$playServicesLocation_version"
57 |
58 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
59 | implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
60 |
61 | implementation "org.koin:koin-android:$koin_version"
62 | implementation "org.koin:koin-androidx-viewmodel:$koin_version"
63 |
64 | implementation "com.squareup.okhttp3:logging-interceptor:$okHttp_version"
65 | implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
66 | implementation "com.squareup.retrofit2:converter-moshi:$moshiConverter_version"
67 | implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
68 |
69 | implementation "com.jakewharton.timber:timber:$timber_version"
70 |
71 | testImplementation 'junit:junit:4.13.1'
72 | testImplementation 'androidx.test:core:1.3.0'
73 | testImplementation 'androidx.arch.core:core-testing:2.1.0'
74 | testImplementation 'org.mockito:mockito-core:3.6.0'
75 | testImplementation 'org.mockito:mockito-inline:3.5.13'
76 | testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
77 | testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
78 |
79 | androidTestImplementation 'androidx.test:rules:1.3.0'
80 | androidTestImplementation 'androidx.test:runner:1.3.0'
81 | androidTestImplementation 'androidx.test.ext:junit:1.1.2'
82 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
83 | }
84 |
--------------------------------------------------------------------------------
/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/androidTest/java/com/gts/trackmypath/MainActivityTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath
2 |
3 | import org.junit.Test
4 | import org.junit.runner.RunWith
5 |
6 | import androidx.test.core.app.ActivityScenario
7 | import androidx.test.ext.junit.runners.AndroidJUnit4
8 | import androidx.test.espresso.Espresso.onView
9 | import androidx.test.espresso.matcher.ViewMatchers
10 | import androidx.test.espresso.assertion.ViewAssertions
11 |
12 | import com.gts.trackmypath.presentation.MainActivity
13 |
14 | @RunWith(AndroidJUnit4::class)
15 | class MainActivityTest {
16 |
17 | @Test
18 | fun whenLaunchApp_ThenShowRightView() {
19 | ActivityScenario.launch(MainActivity::class.java)
20 |
21 | onView(ViewMatchers.withId(R.id.buttonStart)).check(
22 | ViewAssertions.matches(
23 | ViewMatchers.isDisplayed()
24 | )
25 | )
26 |
27 | onView(ViewMatchers.withId(R.id.imageRecyclerView)).check(
28 | ViewAssertions.matches(
29 | ViewMatchers.isDisplayed()
30 | )
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
19 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath
2 |
3 | import androidx.room.Room
4 |
5 | import org.koin.dsl.module
6 | import org.koin.androidx.viewmodel.dsl.viewModel
7 | import org.koin.android.ext.koin.androidApplication
8 |
9 | import okhttp3.OkHttpClient
10 | import okhttp3.logging.HttpLoggingInterceptor
11 |
12 | import retrofit2.Retrofit
13 | import retrofit2.converter.moshi.MoshiConverterFactory
14 |
15 | import com.squareup.moshi.Moshi
16 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
17 |
18 | import com.gts.trackmypath.data.network.FlickrApi
19 | import com.gts.trackmypath.data.network.FlickrClient
20 | import com.gts.trackmypath.data.network.FlickrClientImpl
21 | import com.gts.trackmypath.data.PhotoRepositoryImpl
22 | import com.gts.trackmypath.data.database.PhotoDatabase
23 | import com.gts.trackmypath.domain.LocationServiceInteractor
24 | import com.gts.trackmypath.domain.PhotoRepository
25 | import com.gts.trackmypath.domain.usecase.ClearPhotosUseCase
26 | import com.gts.trackmypath.domain.usecase.RetrievePhotosUseCase
27 | import com.gts.trackmypath.domain.usecase.SearchPhotoByLocationUseCase
28 | import com.gts.trackmypath.presentation.PhotoStreamViewModel
29 |
30 | // declare a module
31 | val appModule = module {
32 | // Define single instance of Retrofit
33 | single { provideFlickrApi().create(FlickrApi::class.java) }
34 | // Define single instance of RoomDatabase.Builder
35 | // RoomDatabase.Builder for a persistent database
36 | // Once a database is built, you should keep a reference to it and re-use it
37 | single { Room.databaseBuilder(androidApplication(), PhotoDatabase::class.java, "photo-db").build() }
38 | // Define single instance of PhotoDatabase
39 | single { get().photoDao() }
40 | // Define single instance of type FlickrClient
41 | // Resolve constructor dependencies with get(), here we need a flickrApi
42 | single { FlickrClientImpl(flickrApi = get()) }
43 | // Define single instance of type PhotoRepository
44 | // Resolve constructor dependencies with get(), here we need a flickrApi and photoDao
45 | single {
46 | PhotoRepositoryImpl(flickrClient = get(), photoDao = get())
47 | }
48 | // Define single instance of SearchPhotoByLocationUseCase
49 | // Resolve constructor dependencies with get(), here we need a photoRepository
50 | single {
51 | SearchPhotoByLocationUseCase(
52 | photoRepository = get()
53 | )
54 | }
55 | // Define single instance of ClearPhotosUseCase
56 | // Resolve constructor dependencies with get(), here we need a photoRepository
57 | single {
58 | ClearPhotosUseCase(
59 | photoRepository = get()
60 | )
61 | }
62 | // Define single instance of RetrievePhotosUseCase
63 | // Resolve constructor dependencies with get(), here we need a photoRepository
64 | single {
65 | RetrievePhotosUseCase(
66 | photoRepository = get()
67 | )
68 | }
69 | // Define single instance of LocationServiceInteractor
70 | // Resolve constructor dependencies with get(), here we need a ClearPhotosUseCase,
71 | // and a SearchPhotoByLocationUseCase
72 | single {
73 | LocationServiceInteractor(
74 | clearPhotosUseCase = get(),
75 | searchPhotoByLocationUseCase = get()
76 | )
77 | }
78 | // Define ViewModel and resolve constructor dependencies with get(),
79 | // here we need retrievePhotosUseCase
80 | viewModel { PhotoStreamViewModel(retrievePhotosUseCase = get()) }
81 | }
82 |
83 | private val okHttpClient = OkHttpClient.Builder()
84 | .addInterceptor(run {
85 | val httpLoggingInterceptor = HttpLoggingInterceptor()
86 | httpLoggingInterceptor.apply {
87 | httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
88 | }
89 | })
90 | .build()
91 |
92 | private val jsonMoshi = Moshi.Builder()
93 | .add(KotlinJsonAdapterFactory())
94 | .build()
95 |
96 | private fun provideFlickrApi(): Retrofit {
97 | return Retrofit.Builder()
98 | .addConverterFactory(MoshiConverterFactory.create(jsonMoshi))
99 | .baseUrl("https://api.flickr.com/")
100 | .client(okHttpClient)
101 | .build()
102 | }
103 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/TrackMyPathApplication.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath
2 |
3 | import android.app.Application
4 | import com.facebook.drawee.backends.pipeline.Fresco
5 |
6 | import org.koin.core.context.startKoin
7 | import org.koin.android.ext.koin.androidLogger
8 | import org.koin.android.ext.koin.androidContext
9 | import org.koin.core.logger.Level
10 |
11 | import timber.log.Timber
12 |
13 | class TrackMyPathApplication : Application() {
14 | override fun onCreate() {
15 | super.onCreate()
16 | Fresco.initialize(this)
17 |
18 | if (BuildConfig.DEBUG) {
19 | Timber.plant(Timber.DebugTree())
20 | }
21 |
22 | startKoin {
23 | // use AndroidLogger as Koin Logger - default Level.INFO
24 | androidLogger(Level.ERROR) //TODO: remove Level.ERROR
25 | // use the Android context given there
26 | androidContext(this@TrackMyPathApplication)
27 | // module list
28 | modules(listOf(appModule))
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/common/Result.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.common
2 |
3 | /**
4 | * A generic class that holds a value with its loading status.
5 | * @param
6 | */
7 | sealed class Result {
8 |
9 | data class Success(val data: T) : Result()
10 | data class Error(val exception: Exception) : Result()
11 |
12 | override fun toString(): String {
13 | return when (this) {
14 | is Success<*> -> "Success[data=$data]"
15 | is Error -> "Error[exception=$exception]"
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/PhotoRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data
2 |
3 | import java.io.IOException
4 | import java.lang.Exception
5 |
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.withContext
8 |
9 | import com.gts.trackmypath.common.Result
10 | import com.gts.trackmypath.data.database.PhotoDao
11 | import com.gts.trackmypath.data.database.PhotoEntity
12 | import com.gts.trackmypath.data.database.toDomainModel
13 | import com.gts.trackmypath.data.network.toDomainModel
14 | import com.gts.trackmypath.data.network.toPhotoEntity
15 | import com.gts.trackmypath.data.network.FlickrClient
16 | import com.gts.trackmypath.data.network.PhotoResponseEntity
17 | import com.gts.trackmypath.domain.model.Photo
18 | import com.gts.trackmypath.domain.PhotoRepository
19 |
20 | import timber.log.Timber
21 |
22 | class PhotoRepositoryImpl(
23 | private val flickrClient: FlickrClient,
24 | private val photoDao: PhotoDao
25 | ) : PhotoRepository {
26 |
27 | override suspend fun searchPhotoByLocation(lat: String, lon: String): Result {
28 | return try {
29 | // Radius used for geo queries, greater than zero and less than 20 miles (or 32 kilometers),
30 | // for use with point-based geo queries. The default value is 5 (km).
31 | // Set a radius of 100 meters. (default unit is km)
32 | return when (val response = flickrClient.searchPhoto(lat, lon, "0.1")) {
33 | is Result.Success -> {
34 | val photosFromDb = photoDao.selectAllPhotos()
35 | // if photo exists in DB, then take the next from the response
36 | val photoFromFlickr = findUniquePhoto(response.data, photosFromDb)
37 | photoFromFlickr?.let {
38 | // save it in the DB
39 | photoDao.insert(photoFromFlickr.toPhotoEntity())
40 | Result.Success(photoFromFlickr.toDomainModel())
41 | } ?: throw IOException("no photos retrieved from flickr")
42 | }
43 | is Result.Error -> {
44 | Result.Error(response.exception)
45 | }
46 | else -> Result.Error(Exception("unknown exception"))
47 | }
48 | } catch (e: Exception) {
49 | Timber.e(e, "searchPhotoByLocation exception")
50 | Result.Error(IOException("searchPhotoByLocation exception", e))
51 | }
52 | }
53 |
54 | // Retrieve all photos from the DB
55 | override suspend fun loadAllPhotos(): Result> {
56 | val photos = photoDao.selectAllPhotos()
57 | return if (photos.isNotEmpty()) {
58 | val result = photos.map { photoEntity -> photoEntity.toDomainModel() }
59 | Result.Success(result)
60 | } else {
61 | Result.Error(IOException("Failed to retrieve photos from database"))
62 | }
63 | }
64 |
65 | // Delete all photos in DB
66 | override suspend fun deletePhotos() {
67 | try {
68 | photoDao.deletePhotos()
69 | } catch (e: Exception) {
70 | Timber.e(e, "deletePhotos exception")
71 | }
72 | }
73 |
74 | private suspend fun findUniquePhoto(
75 | photosFromFlickr: List,
76 | photosFromDb: Array
77 | ): PhotoResponseEntity? {
78 | return withContext(Dispatchers.Default) {
79 | if (photosFromDb.isNotEmpty()) {
80 | val iterator = photosFromFlickr.iterator()
81 | while (iterator.hasNext()) {
82 | val photoResponseEntity = iterator.next()
83 | Timber.d("fetched photo id ${photoResponseEntity.id}")
84 | if (photosFromDb.all { photoEntity -> photoEntity.id != photoResponseEntity.id }) {
85 | return@withContext photoResponseEntity
86 | }
87 | }
88 | } else {
89 | return@withContext photosFromFlickr[0]
90 | }
91 | return@withContext null
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/database/PhotoDao.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.database
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Query
5 | import androidx.room.Insert
6 | import androidx.room.OnConflictStrategy
7 |
8 | // Data Access Objects (DAO) are the main classes where you define your database interactions.
9 | // They can include a variety of query methods.
10 | @Dao
11 | interface PhotoDao {
12 | @Insert(onConflict = OnConflictStrategy.IGNORE)
13 | suspend fun insert(photo: PhotoEntity)
14 |
15 | @Query("SELECT * FROM photos ORDER BY photoId DESC")
16 | suspend fun selectAllPhotos(): Array
17 |
18 | @Query("DELETE FROM photos")
19 | suspend fun deletePhotos()
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/database/PhotoDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 |
6 | @Database(entities = [PhotoEntity::class], version = 1 )
7 | abstract class PhotoDatabase : RoomDatabase() {
8 |
9 | abstract fun photoDao(): PhotoDao
10 | }
11 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/database/PhotoEntity.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.database
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 | import androidx.room.ColumnInfo
6 |
7 | import com.gts.trackmypath.domain.model.Photo
8 |
9 | @Entity(tableName = "photos")
10 | class PhotoEntity(
11 | @PrimaryKey(autoGenerate = true) var photoId: Int,
12 | @ColumnInfo(name = "id") var id: String = "",
13 | @ColumnInfo(name = "secret") var secret: String = "",
14 | @ColumnInfo(name = "server") var server: String = "",
15 | @ColumnInfo(name = "farm") var farm: String = ""
16 | )
17 |
18 | fun PhotoEntity.toDomainModel() = Photo(
19 | id = id,
20 | secret = secret,
21 | server = server,
22 | farm = farm
23 | )
24 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/network/FlickrApi.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.network
2 |
3 | import retrofit2.Response
4 | import retrofit2.http.GET
5 | import retrofit2.http.Query
6 |
7 | internal const val URLS = "url_sq, url_t, url_s, url_q, url_m, url_n, url_z, url_c, url_l, url_o"
8 |
9 | interface FlickrApi {
10 |
11 | @GET("$ENDPOINT$METHOD_PHOTOS_SEARCH$EXTRA_PARAMS")
12 | suspend fun search(
13 | @Query("api_key") apiKey: String,
14 | @Query("lat") lat: String? = null,
15 | @Query("lon") lon: String? = null,
16 | @Query("radius ") radius: String? = null,
17 | @Query("extras") extras: String = URLS
18 | ): Response
19 |
20 | companion object {
21 | const val API_KEY = "58cace78ed8d64e8490e5e3341a96930"
22 | private const val ENDPOINT = "https://www.flickr.com/services/rest/"
23 | private const val METHOD_PHOTOS_SEARCH = "?method=flickr.photos.search"
24 | private const val EXTRA_PARAMS = "&nojsoncallback=1&format=json"
25 | private const val EXTRA_SMALL_URL = "url_s"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/network/FlickrClient.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.network
2 |
3 | import com.gts.trackmypath.common.Result
4 |
5 | interface FlickrClient {
6 |
7 | suspend fun searchPhoto(
8 | lat: String,
9 | lon: String,
10 | radius: String
11 | ): Result>
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/network/FlickrClientImpl.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.network
2 |
3 | import java.io.IOException
4 | import java.lang.Exception
5 |
6 | import com.gts.trackmypath.common.Result
7 |
8 | import timber.log.Timber
9 |
10 | class FlickrClientImpl(private val flickrApi: FlickrApi) : FlickrClient {
11 |
12 | // request a photo from flickr service based on current location
13 | override suspend fun searchPhoto(
14 | lat: String,
15 | lon: String,
16 | radius: String
17 | ): Result> {
18 | return try {
19 | val response = flickrApi.search(FlickrApi.API_KEY, lat, lon, radius)
20 | if (response.isSuccessful) {
21 | val data = response.body()
22 | if (data != null) {
23 | // return the first result from response
24 | return Result.Success(data.photosResponseEntity.list)
25 | } else {
26 | Timber.e("searchPhotoByLocation data error")
27 | Result.Error(IOException("searchPhotoByLocation data error"))
28 | }
29 | } else {
30 | Timber.e("searchPhotoByLocation response error")
31 | Result.Error(IOException("searchPhotoByLocation response error"))
32 | }
33 | } catch (e: Exception) {
34 | Timber.e(e, "searchPhotoByLocation exception")
35 | Result.Error(IOException("searchPhotoByLocation exception", e))
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/data/network/ResponseEntity.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.network
2 |
3 | import com.squareup.moshi.Json
4 |
5 | import com.gts.trackmypath.data.database.PhotoEntity
6 | import com.gts.trackmypath.domain.model.Photo
7 |
8 | data class ResponseEntity(
9 | @Json(name = "photos")
10 | val photosResponseEntity: PhotosResponseEntity
11 | )
12 |
13 | data class PhotosResponseEntity(
14 | @Json(name = "page")
15 | val page: Int,
16 | @Json(name = "pages")
17 | val pages: Int,
18 | @Json(name = "perpage")
19 | val perpage: String,
20 | @Json(name = "total")
21 | val total: String,
22 | @Json(name = "photo")
23 | val list: List
24 | )
25 |
26 | data class PhotoResponseEntity(
27 | @Json(name = "id")
28 | val id: String,
29 | @Json(name = "secret")
30 | val secret: String,
31 | @Json(name = "server")
32 | val server: String,
33 | @Json(name = "farm")
34 | val farm: String
35 | )
36 |
37 | // map to DB entity, PhotoEntity
38 | fun PhotoResponseEntity.toPhotoEntity() = PhotoEntity(
39 | photoId = 0,
40 | id = id,
41 | secret = secret,
42 | server = server,
43 | farm = farm
44 | )
45 |
46 | fun PhotoResponseEntity.toDomainModel() = Photo(
47 | id = id,
48 | secret = secret,
49 | server = server,
50 | farm = farm
51 | )
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/LocationServiceInteractor.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain
2 |
3 | import com.gts.trackmypath.common.Result
4 | import com.gts.trackmypath.domain.model.Photo
5 | import com.gts.trackmypath.domain.usecase.ClearPhotosUseCase
6 | import com.gts.trackmypath.domain.usecase.SearchPhotoByLocationUseCase
7 |
8 | class LocationServiceInteractor(
9 | private val clearPhotosUseCase: ClearPhotosUseCase,
10 | private val searchPhotoByLocationUseCase: SearchPhotoByLocationUseCase
11 | ) {
12 |
13 | suspend fun clearPhotosFromList() {
14 | clearPhotosUseCase.invoke()
15 | }
16 |
17 | suspend fun getPhotoBasedOnLocation(latitude: Double, longitude: Double): Result {
18 | return searchPhotoByLocationUseCase.invoke(latitude, longitude)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/PhotoRepository.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain
2 |
3 | import com.gts.trackmypath.common.Result
4 | import com.gts.trackmypath.domain.model.Photo
5 |
6 | // Repository modules handle data operations.
7 | // They provide a clean API so that the rest of the app can retrieve this data easily.
8 | interface PhotoRepository {
9 |
10 | suspend fun searchPhotoByLocation(lat: String, lon: String): Result
11 |
12 | suspend fun loadAllPhotos(): Result>
13 |
14 | suspend fun deletePhotos()
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/model/Photo.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain.model
2 |
3 | data class Photo(
4 | val id: String,
5 | val secret: String,
6 | val server: String,
7 | val farm: String
8 | )
9 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/usecase/ClearPhotosUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain.usecase
2 |
3 | import com.gts.trackmypath.domain.PhotoRepository
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 |
7 | class ClearPhotosUseCase(private val photoRepository: PhotoRepository) {
8 |
9 | suspend operator fun invoke() = withContext(Dispatchers.IO) {
10 | photoRepository.deletePhotos()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/usecase/RetrievePhotosUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain.usecase
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 |
6 | import com.gts.trackmypath.common.Result
7 | import com.gts.trackmypath.domain.PhotoRepository
8 | import com.gts.trackmypath.domain.model.Photo
9 |
10 | class RetrievePhotosUseCase(private val photoRepository: PhotoRepository) {
11 |
12 | suspend operator fun invoke(): Result> = withContext(Dispatchers.IO) {
13 | return@withContext photoRepository.loadAllPhotos()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/domain/usecase/SearchPhotoByLocationUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain.usecase
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.withContext
5 |
6 | import com.gts.trackmypath.common.Result
7 | import com.gts.trackmypath.domain.PhotoRepository
8 | import com.gts.trackmypath.domain.model.Photo
9 |
10 | class SearchPhotoByLocationUseCase(private val photoRepository: PhotoRepository) {
11 |
12 | suspend operator fun invoke(lat: Double, lon: Double): Result = withContext(Dispatchers.IO) {
13 | return@withContext photoRepository.searchPhotoByLocation(lat.toString(), lon.toString())
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import android.os.Bundle
4 | import androidx.appcompat.app.AppCompatActivity
5 |
6 | import com.gts.trackmypath.R
7 |
8 | class MainActivity : AppCompatActivity() {
9 |
10 | override fun onCreate(savedInstanceState: Bundle?) {
11 | super.onCreate(savedInstanceState)
12 | setContentView(R.layout.activity_main)
13 | if (savedInstanceState == null) {
14 | supportFragmentManager.beginTransaction()
15 | .replace(R.id.container, PhotoStreamFragment.newInstance())
16 | .commitNow()
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/PhotoAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import android.view.ViewGroup
4 | import android.view.LayoutInflater
5 | import androidx.recyclerview.widget.RecyclerView
6 |
7 | import com.facebook.drawee.view.SimpleDraweeView
8 |
9 | import com.gts.trackmypath.databinding.PhotoItemBinding
10 | import com.gts.trackmypath.presentation.model.PhotoViewItem
11 |
12 | class PhotoAdapter(private val photos: MutableList) :
13 | RecyclerView.Adapter() {
14 |
15 | private var _binding: PhotoItemBinding? = null
16 | private val binding get() = _binding
17 |
18 | fun addPhoto(photo: PhotoViewItem) {
19 | // Add the photo at the beginning of the list
20 | photos.add(0, photo)
21 | notifyItemInserted(0)
22 | }
23 |
24 | fun populate(photoItems: List) {
25 | photos.addAll(photoItems)
26 | notifyDataSetChanged()
27 | }
28 |
29 | fun resetPhotoList() {
30 | photos.clear()
31 | notifyDataSetChanged()
32 | }
33 |
34 | override fun onCreateViewHolder(viewGroup: ViewGroup, i: Int): PhotoViewHolder {
35 | _binding =
36 | PhotoItemBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false)
37 | return PhotoViewHolder(binding)
38 | }
39 |
40 | override fun onBindViewHolder(holder: PhotoViewHolder, position: Int) {
41 | val photo = photos[position]
42 | holder.photoImageView.setImageURI(
43 | buildUri(
44 | photo.farm,
45 | photo.server,
46 | photo.id,
47 | photo.secret
48 | )
49 | )
50 | }
51 |
52 | // override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
53 | // super.onDetachedFromRecyclerView(recyclerView)
54 | // _binding = null // we actually need this part
55 | // }
56 |
57 | override fun getItemCount(): Int = photos.size
58 |
59 | class PhotoViewHolder(binding: PhotoItemBinding?) : RecyclerView.ViewHolder(binding!!.root) {
60 | var photoImageView: SimpleDraweeView = binding?.imageViewId as SimpleDraweeView
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/PhotoStreamFragment.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.content.IntentFilter
7 | import android.content.ComponentName
8 | import android.content.ServiceConnection
9 | import android.content.SharedPreferences
10 | import android.content.BroadcastReceiver
11 | import android.content.pm.PackageManager
12 | import android.os.Bundle
13 | import android.os.IBinder
14 | import android.net.Uri
15 | import android.view.View
16 | import android.view.ViewGroup
17 | import android.view.LayoutInflater
18 | import android.provider.Settings
19 | import androidx.lifecycle.Observer
20 | import androidx.fragment.app.Fragment
21 | import androidx.core.app.ActivityCompat
22 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
23 | import androidx.preference.PreferenceManager
24 |
25 | import java.util.ArrayList
26 |
27 | import com.google.android.material.snackbar.Snackbar
28 |
29 | import org.koin.androidx.viewmodel.ext.android.viewModel
30 |
31 | import com.gts.trackmypath.R
32 | import com.gts.trackmypath.BuildConfig
33 | import com.gts.trackmypath.databinding.FragmentPhotoStreamBinding
34 | import com.gts.trackmypath.presentation.model.PhotoViewItem
35 | import com.gts.trackmypath.presentation.service.LocationService
36 |
37 | import timber.log.Timber
38 |
39 | class PhotoStreamFragment : Fragment() {
40 |
41 | companion object {
42 | fun newInstance() = PhotoStreamFragment()
43 |
44 | // Used in checking for runtime permissions.
45 | private const val REQUEST_PERMISSIONS_REQUEST_CODE = 34
46 | }
47 |
48 | private val viewModel: PhotoStreamViewModel by viewModel()
49 |
50 | // The BroadcastReceiver used to listen from broadcasts from the service.
51 | private lateinit var locationReceiver: LocationReceiver
52 |
53 | // A reference to the service used to get location updates.
54 | private lateinit var locationService: LocationService
55 |
56 | // used to store button state
57 | private lateinit var sharedPref: SharedPreferences
58 |
59 | // recycler view and adapter for retrieved photos
60 | private lateinit var photoAdapter: PhotoAdapter
61 |
62 | private lateinit var binding: FragmentPhotoStreamBinding
63 |
64 | // Monitors the state of the connection to the service.
65 | private val serviceConnection = object : ServiceConnection {
66 |
67 | override fun onServiceConnected(name: ComponentName, service: IBinder) {
68 | Timber.d("ServiceConnection: onServiceConnected")
69 | val binder = service as LocationService.LocalBinder
70 | locationService = binder.service
71 | }
72 |
73 | override fun onServiceDisconnected(name: ComponentName) {
74 | Timber.d("ServiceConnection: onServiceDisconnected")
75 | }
76 | }
77 |
78 | override fun onCreate(savedInstanceState: Bundle?) {
79 | super.onCreate(savedInstanceState)
80 |
81 | sharedPref = activity?.getPreferences(Context.MODE_PRIVATE) ?: return
82 | locationReceiver = LocationReceiver()
83 | }
84 |
85 | override fun onCreateView(
86 | inflater: LayoutInflater,
87 | container: ViewGroup?,
88 | savedInstanceState: Bundle?
89 | ): View? {
90 | binding = FragmentPhotoStreamBinding.inflate(layoutInflater, container, false)
91 | return binding.root
92 | }
93 |
94 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
95 | super.onViewCreated(view, savedInstanceState)
96 |
97 | val photoList = ArrayList()
98 | photoAdapter = PhotoAdapter(photoList)
99 | binding.imageRecyclerView.adapter = photoAdapter
100 | binding.imageRecyclerView.isNestedScrollingEnabled = false
101 |
102 | viewModel.photosByLocation.observe(viewLifecycleOwner, Observer { photos ->
103 | photoAdapter.populate(photos)
104 | binding.imageRecyclerView.smoothScrollToPosition(0)
105 | })
106 | }
107 |
108 | override fun onStart() {
109 | super.onStart()
110 |
111 | photoAdapter.resetPhotoList()
112 | viewModel.retrievePhotos()
113 |
114 | binding.buttonStart.text = PreferenceManager.getDefaultSharedPreferences(context)
115 | .getString(getString(R.string.service_state), "Start")
116 | binding.buttonStart.setOnClickListener {
117 | if (binding.buttonStart.text == getString(R.string.button_text_stop)) {
118 | locationService.removeLocationUpdates()
119 | binding.buttonStart.text = getString(R.string.button_text_start)
120 | } else {
121 | if (!checkPermissions()) {
122 | requestPermissions()
123 | } else {
124 | locationService.requestLocationUpdates()
125 | }
126 | photoAdapter.resetPhotoList()
127 | binding.buttonStart.text = getString(R.string.button_text_stop)
128 | }
129 | }
130 |
131 | // Bind to the service. If the service is in foreground mode, this signals to the service
132 | // that since this activity is in the foreground, the service can exit foreground mode.
133 | requireActivity().bindService(
134 | Intent(context, LocationService::class.java),
135 | serviceConnection,
136 | Context.BIND_AUTO_CREATE
137 | )
138 | }
139 |
140 | override fun onResume() {
141 | super.onResume()
142 | LocalBroadcastManager.getInstance(requireContext()).registerReceiver(
143 | locationReceiver,
144 | IntentFilter(LocationService.ACTION_BROADCAST)
145 | )
146 | }
147 |
148 | override fun onPause() {
149 | LocalBroadcastManager.getInstance(requireContext()).unregisterReceiver(locationReceiver)
150 | super.onPause()
151 | }
152 |
153 | override fun onStop() {
154 | // Unbind from the service. This signals to the service that this activity is no longer in the foreground,
155 | // and the service can respond by promoting itself to a foreground service.
156 | requireActivity().unbindService(serviceConnection)
157 | super.onStop()
158 | }
159 |
160 | /**
161 | * Returns the current state of the permissions needed.
162 | */
163 | private fun checkPermissions(): Boolean {
164 | return PackageManager.PERMISSION_GRANTED == ActivityCompat.checkSelfPermission(
165 | requireContext(),
166 | Manifest.permission.ACCESS_FINE_LOCATION
167 | )
168 | }
169 |
170 | private fun requestPermissions() {
171 | val shouldProvideRationale = ActivityCompat.shouldShowRequestPermissionRationale(
172 | requireActivity(),
173 | Manifest.permission.ACCESS_FINE_LOCATION
174 | )
175 |
176 | // Provide an additional rationale to the user. This would happen if the user denied the
177 | // request previously, but didn't check the "Don't ask again" checkbox.
178 | if (shouldProvideRationale) {
179 | Snackbar.make(
180 | binding.fragmentMain,
181 | getString(R.string.location_permission_text),
182 | Snackbar.LENGTH_INDEFINITE
183 | ).setAction(getString(R.string.location_permission_action_ok_text)) {
184 | requestPermissions(
185 | arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
186 | REQUEST_PERMISSIONS_REQUEST_CODE
187 | )
188 | }.show()
189 | } else {
190 | // Request permission. It's possible this can be auto answered if device policy sets the permission
191 | // in a given state or the user denied the permission previously and checked "Never ask again".
192 | requestPermissions(
193 | arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
194 | REQUEST_PERMISSIONS_REQUEST_CODE
195 | )
196 | }
197 | }
198 |
199 | /**
200 | * Callback received when a permissions request has been completed.
201 | */
202 | override fun onRequestPermissionsResult(
203 | requestCode: Int,
204 | permissions: Array,
205 | grantResults: IntArray
206 | ) {
207 | if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) {
208 | when {
209 | grantResults.isEmpty() ->
210 | // If user interaction was interrupted, the permission request is cancelled
211 | // and you receive empty arrays.
212 | Timber.i("=======> User interaction was cancelled.")
213 | grantResults[0] == PackageManager.PERMISSION_GRANTED -> {
214 | locationService.requestLocationUpdates()
215 | }
216 | else -> // Permission denied.
217 | Snackbar.make(
218 | binding.fragmentMain,
219 | getString(R.string.location_permission_denied_text),
220 | Snackbar.LENGTH_INDEFINITE
221 | )
222 | .setAction(getString(R.string.location_permission_action_settings_text)) {
223 | // Build intent that displays the App settings screen.
224 | val intent = Intent()
225 | intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
226 | val uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
227 | intent.data = uri
228 | intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
229 | startActivity(intent)
230 | }.show()
231 | }
232 | }
233 | }
234 |
235 | /**
236 | * Receiver for broadcasts sent by [LocationService].
237 | */
238 | private inner class LocationReceiver : BroadcastReceiver() {
239 | override fun onReceive(context: Context, intent: Intent) {
240 | val photo = intent.getParcelableExtra(LocationService.EXTRA_PHOTO)
241 | if (photo != null) {
242 | photoAdapter.addPhoto(photo)
243 | binding.imageRecyclerView.smoothScrollToPosition(0)
244 | }
245 | }
246 | }
247 | }
248 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/PhotoStreamViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.ViewModel
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.viewModelScope
7 |
8 | import kotlinx.coroutines.launch
9 |
10 | import com.gts.trackmypath.common.Result
11 | import com.gts.trackmypath.domain.usecase.RetrievePhotosUseCase
12 | import com.gts.trackmypath.presentation.model.PhotoViewItem
13 | import com.gts.trackmypath.presentation.model.toPresentationModel
14 |
15 | import timber.log.Timber
16 |
17 | class PhotoStreamViewModel(
18 | private val retrievePhotosUseCase: RetrievePhotosUseCase
19 | ) : ViewModel() {
20 |
21 | private val _photosByLocation = MutableLiveData>()
22 | val photosByLocation: LiveData>
23 | get() = _photosByLocation
24 |
25 | fun retrievePhotos() {
26 | viewModelScope.launch {
27 | when (val result = retrievePhotosUseCase.invoke()) {
28 | is Result.Success -> {
29 | _photosByLocation.postValue(result.data.map { it.toPresentationModel() })
30 | }
31 | is Result.Error -> Timber.d("no photos")
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/Utils.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | fun buildUri(farm: String, server: String, id: String, secret: String): String {
4 | return ("https://farm$farm.staticflickr.com/$server/${id}_$secret.jpg")
5 | }
6 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/model/PhotoViewItem.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation.model
2 |
3 | import android.os.Parcelable
4 |
5 | import kotlinx.parcelize.Parcelize
6 |
7 | import com.gts.trackmypath.domain.model.Photo
8 |
9 | @Parcelize
10 | data class PhotoViewItem(
11 | val id: String,
12 | val secret: String,
13 | val server: String,
14 | val farm: String
15 | ) : Parcelable
16 |
17 | fun Photo.toPresentationModel() = PhotoViewItem(
18 | id = id,
19 | secret = secret,
20 | server = server,
21 | farm = farm
22 | )
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/gts/trackmypath/presentation/service/LocationService.kt:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Google Inc. All Rights Reserved.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.gts.trackmypath.presentation.service
18 |
19 | import android.os.Build
20 | import android.os.Binder
21 | import android.os.Looper
22 | import android.os.IBinder
23 | import android.os.Handler
24 | import android.os.HandlerThread
25 | import android.app.ActivityManager
26 | import android.app.PendingIntent
27 | import android.app.Notification
28 | import android.app.NotificationChannel
29 | import android.app.NotificationManager
30 | import android.content.Intent
31 | import android.content.Context
32 | import android.location.Location
33 | import androidx.core.app.NotificationCompat
34 | import androidx.lifecycle.LifecycleService
35 | import androidx.preference.PreferenceManager
36 | import androidx.localbroadcastmanager.content.LocalBroadcastManager
37 |
38 | import com.google.android.gms.location.LocationResult
39 | import com.google.android.gms.location.LocationRequest
40 | import com.google.android.gms.location.LocationServices
41 | import com.google.android.gms.location.LocationCallback
42 | import com.google.android.gms.location.FusedLocationProviderClient
43 |
44 | import kotlinx.coroutines.SupervisorJob
45 | import kotlinx.coroutines.CoroutineScope
46 | import kotlinx.coroutines.Dispatchers
47 | import kotlinx.coroutines.launch
48 |
49 | import org.koin.android.ext.android.inject
50 |
51 | import com.gts.trackmypath.R
52 | import com.gts.trackmypath.common.Result
53 | import com.gts.trackmypath.domain.LocationServiceInteractor
54 | import com.gts.trackmypath.presentation.MainActivity
55 | import com.gts.trackmypath.presentation.model.toPresentationModel
56 |
57 | import java.util.concurrent.TimeUnit
58 | import timber.log.Timber
59 |
60 | /**
61 | * A bound and started service that is promoted to a foreground service when location updates have
62 | * been requested and all clients unbind.
63 | *
64 | * This sample show how to use a long-running service for location updates. When an activity is
65 | * bound to this service, frequent location updates are permitted. When the activity is removed
66 | * from the foreground, the service promotes itself to a foreground service, and location updates
67 | * continue. When the activity comes back to the foreground, the foreground service stops, and the
68 | * notification associated with that service is removed.
69 | */
70 | class LocationService : LifecycleService() {
71 |
72 | companion object {
73 | const val EXTRA_PHOTO = "location"
74 | const val ACTION_BROADCAST = "broadcast"
75 |
76 | private val TAG = LocationService::class.java.simpleName
77 |
78 | // The name of the channel for notifications.
79 | private const val NOTIFICATION_CHANNEL_ID = "channel_01"
80 |
81 | // The identifier for the notification displayed for the foreground service.
82 | private const val NOTIFICATION_ID = 1101
83 | private const val EXTRA_STARTED_FROM_NOTIFICATION = "started_from_notification"
84 |
85 | private const val SMALLEST_DISPLACEMENT_100_METERS = 100F
86 | private const val INTERVAL_TIME = 60
87 | private const val FASTEST_INTERVAL_TIME = 30
88 | }
89 |
90 | private val binder = LocalBinder()
91 |
92 | // A data object that contains quality of service parameters for requests
93 | private lateinit var locationRequest: LocationRequest
94 |
95 | // Provides access to the Fused Location Provider API.
96 | private lateinit var fusedLocationClient: FusedLocationProviderClient
97 |
98 | // Callback for changes in location.
99 | private lateinit var locationCallback: LocationCallback
100 | private lateinit var serviceHandler: Handler
101 |
102 | // The current location.
103 | private lateinit var location: Location
104 | private lateinit var notificationManager: NotificationManager
105 |
106 | private val locationServiceInteractor: LocationServiceInteractor by inject()
107 | private val job = SupervisorJob()
108 | private val scope = CoroutineScope(Dispatchers.IO + job)
109 |
110 | override fun onCreate() {
111 | super.onCreate()
112 | fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
113 |
114 | locationCallback = object : LocationCallback() {
115 | override fun onLocationResult(locationResult: LocationResult?) {
116 | super.onLocationResult(locationResult)
117 | onNewLocation(locationResult!!.lastLocation)
118 | }
119 | }
120 |
121 | createLocationRequest()
122 | getLastLocation()
123 |
124 | val handlerThread = HandlerThread(TAG)
125 | handlerThread.start()
126 | serviceHandler = Handler(handlerThread.looper)
127 | notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
128 |
129 | // Android O requires a Notification Channel.
130 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
131 | val name = getString(R.string.app_name)
132 | // Create the channel for the notification
133 | val mChannel = NotificationChannel(
134 | NOTIFICATION_CHANNEL_ID,
135 | name,
136 | NotificationManager.IMPORTANCE_DEFAULT
137 | )
138 |
139 | // Set the Notification Channel for the Notification Manager.
140 | notificationManager.createNotificationChannel(mChannel)
141 | }
142 | }
143 |
144 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
145 | Timber.tag(TAG).i("LocationService started")
146 | val startedFromNotification = intent?.getBooleanExtra(
147 | EXTRA_STARTED_FROM_NOTIFICATION,
148 | false
149 | )
150 |
151 | // We got here because the user decided to remove location updates from the notification.
152 | if (startedFromNotification!!) {
153 | removeLocationUpdates()
154 | stopSelf()
155 | }
156 | // Tells the system to not try to recreate the service after it has been killed.
157 | return super.onStartCommand(intent, flags, START_NOT_STICKY)
158 | }
159 |
160 | override fun onBind(intent: Intent): IBinder {
161 | super.onBind(intent)
162 | // Called when a client (MainActivity in case of this app) comes to the foreground
163 | // and binds with this service.
164 | // The service should cease to be a foreground service when that happens.
165 | Timber.tag(TAG).i("in onBind()")
166 | stopForeground(true)
167 | return binder
168 | }
169 |
170 | override fun onRebind(intent: Intent) {
171 | // Called when a client (MainActivity in case of this sample) returns to the foreground and
172 | // binds once again with this service.
173 | // The service should cease to be a foreground service when that happens.
174 | Timber.tag(TAG).i("in onRebind()")
175 | stopForeground(true)
176 | super.onRebind(intent)
177 | }
178 |
179 | override fun onUnbind(intent: Intent): Boolean {
180 | Timber.tag(TAG).i("Last client unbound from service")
181 | Timber.tag(TAG).i("Starting foreground service")
182 | startForeground(NOTIFICATION_ID, getNotification())
183 | return true // Ensures onRebind() is called when a client re-binds.
184 | }
185 |
186 | override fun onDestroy() {
187 | serviceHandler.removeCallbacksAndMessages(null)
188 | job.cancel()
189 | super.onDestroy()
190 | }
191 |
192 | /**
193 | * Makes a request for location updates. Note that in this sample we merely log the [SecurityException].
194 | */
195 | fun requestLocationUpdates() {
196 | Timber.tag(TAG).i("Requesting location updates")
197 | startService(Intent(applicationContext, LocationService::class.java))
198 | try {
199 | saveServiceState(getString(R.string.service_state_started))
200 | fusedLocationClient.requestLocationUpdates(
201 | locationRequest, locationCallback, Looper.myLooper()
202 | )
203 | scope.launch {
204 | locationServiceInteractor.clearPhotosFromList()
205 | }
206 | } catch (unlikely: SecurityException) {
207 | saveServiceState(getString(R.string.service_state_stopped))
208 | Timber.tag(TAG).e("Lost location permission. Could not request updates. $unlikely")
209 | }
210 | }
211 |
212 | /**
213 | * Removes location updates. Note that in this sample we merely log the [SecurityException].
214 | */
215 | fun removeLocationUpdates() {
216 | Timber.tag(TAG).i("Removing location updates")
217 | try {
218 | saveServiceState(getString(R.string.service_state_stopped))
219 | fusedLocationClient.removeLocationUpdates(locationCallback)
220 | stopSelf()
221 | } catch (unlikely: SecurityException) {
222 | saveServiceState(getString(R.string.service_state_started))
223 | Timber.tag(TAG).e("Lost location permission. Could not remove updates. $unlikely")
224 | }
225 | }
226 |
227 | private fun getLastLocation() {
228 | try {
229 | fusedLocationClient.lastLocation.addOnCompleteListener { task ->
230 | if (task.isSuccessful && task.result != null) {
231 | location = task.result!!
232 | } else {
233 | Timber.tag(TAG).w("Failed to get location.")
234 | }
235 | }
236 | } catch (unlikely: SecurityException) {
237 | Timber.tag(TAG).e("Lost location permission.$unlikely")
238 | }
239 | }
240 |
241 | private fun onNewLocation(location: Location) {
242 | Timber.tag(TAG).i("New location: $location")
243 | this.location = location
244 |
245 | scope.launch {
246 | when (val result = locationServiceInteractor.getPhotoBasedOnLocation(
247 | location.latitude, location.longitude
248 | )
249 | ) {
250 | is Result.Success -> {
251 | // Notify anyone listening for broadcasts about the new photo.
252 | val intent = Intent(ACTION_BROADCAST)
253 | intent.putExtra(EXTRA_PHOTO, result.data.toPresentationModel())
254 | LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
255 | }
256 | is Result.Error -> Timber.tag(TAG).e(" LocationService error!")
257 | }
258 | }
259 |
260 | // Update notification content if running as a foreground service.
261 | if (serviceIsRunningInForeground(this)) {
262 | notificationManager.notify(NOTIFICATION_ID, getNotification())
263 | }
264 | }
265 |
266 | /**
267 | * Sets the location request parameters.
268 | */
269 | private fun createLocationRequest() {
270 | locationRequest = LocationRequest().apply {
271 | priority = LocationRequest.PRIORITY_HIGH_ACCURACY
272 | smallestDisplacement = SMALLEST_DISPLACEMENT_100_METERS // 100 meters
273 | interval = TimeUnit.SECONDS.toMillis(INTERVAL_TIME.toLong())
274 | fastestInterval = TimeUnit.SECONDS.toMillis(FASTEST_INTERVAL_TIME.toLong())
275 | }
276 | }
277 |
278 | /**
279 | * Class used for the client Binder.
280 | * Since this service runs in the same process as its clients, we don't need to deal with IPC.
281 | */
282 | inner class LocalBinder : Binder() {
283 | internal val service: LocationService
284 | get() = this@LocationService
285 | }
286 |
287 | /**
288 | * Returns true if this is a foreground service.
289 | *
290 | * @param context The [Context].
291 | */
292 | @SuppressWarnings("deprecation")
293 | private fun serviceIsRunningInForeground(context: Context): Boolean {
294 | val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
295 | for (service in manager.getRunningServices(Integer.MAX_VALUE)) {
296 | if (javaClass.name == service.service.className) {
297 | if (service.foreground) {
298 | return true
299 | }
300 | }
301 | }
302 | return false
303 | }
304 |
305 | /**
306 | * Returns the [NotificationCompat] used as part of the foreground service.
307 | */
308 | private fun getNotification(): Notification {
309 | val intent = Intent(this, LocationService::class.java)
310 | // Extra to help us figure out if we arrived in onStartCommand via the notification or not.
311 | intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true)
312 | // The PendingIntent that leads to a call to onStartCommand() in this service.
313 | val servicePendingIntent = PendingIntent.getService(
314 | this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
315 | )
316 | // The PendingIntent to launch activity.
317 | val activityPendingIntent = PendingIntent.getActivity(
318 | this, 0, Intent(this, MainActivity::class.java), 0
319 | )
320 |
321 | val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
322 | .addAction(0, getString(R.string.notification_action_launch), activityPendingIntent)
323 | .addAction(0, getString(R.string.notification_action_stop), servicePendingIntent)
324 | .setContentTitle(getString(R.string.notification_content_title))
325 | .setContentText(getString(R.string.notification_content_text))
326 | .setOngoing(true)
327 | .setPriority(1) // Notification.PRIORITY_HIGH
328 | .setSmallIcon(R.mipmap.ic_launcher)
329 | .setWhen(System.currentTimeMillis())
330 |
331 | // Set the Channel ID for Android O.
332 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
333 | builder.setChannelId(NOTIFICATION_CHANNEL_ID) // Channel ID
334 | }
335 |
336 | return builder.build()
337 | }
338 |
339 | /**
340 | * Save the state of the Service.
341 | * The UI retrieves the state to set the Button text.
342 | */
343 | private fun saveServiceState(state: String) {
344 | PreferenceManager.getDefaultSharedPreferences(applicationContext)
345 | .edit()
346 | .putString(getString(R.string.service_state), state)
347 | .apply()
348 | }
349 |
350 | }
351 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
12 |
13 |
19 |
22 |
25 |
26 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
10 |
12 |
14 |
16 |
18 |
20 |
22 |
24 |
26 |
28 |
30 |
32 |
34 |
36 |
38 |
40 |
42 |
44 |
46 |
48 |
50 |
52 |
54 |
56 |
58 |
60 |
62 |
64 |
66 |
68 |
70 |
72 |
74 |
75 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_photo_stream.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
19 |
20 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/photo_item.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #008577
4 | #00574B
5 | #D81B60
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Track my path
3 | SERVICE_STATE
4 | Track my path
5 | application is running
6 | Launch app
7 | Stop
8 | Stop
9 | Start
10 | Start
11 | Stop
12 | Location permission is needed for core functionality
13 | OK
14 | permission denied
15 | Settings
16 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/data/repository/PhotoRepositoryImplTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.data.repository
2 |
3 | import org.junit.Test
4 | import org.junit.Before
5 | import org.junit.Assert
6 |
7 | import com.nhaarman.mockitokotlin2.mock
8 | import com.nhaarman.mockitokotlin2.verify
9 | import com.nhaarman.mockitokotlin2.whenever
10 |
11 | import kotlinx.coroutines.runBlocking
12 |
13 | import com.gts.trackmypath.common.Result
14 | import com.gts.trackmypath.data.PhotoRepositoryImpl
15 | import com.gts.trackmypath.data.database.PhotoDao
16 | import com.gts.trackmypath.data.database.PhotoEntity
17 | import com.gts.trackmypath.data.network.toDomainModel
18 | import com.gts.trackmypath.data.network.toPhotoEntity
19 | import com.gts.trackmypath.data.network.FlickrClient
20 | import com.gts.trackmypath.data.network.PhotoResponseEntity
21 |
22 | class PhotoRepositoryImplTest {
23 |
24 | private lateinit var repository: PhotoRepositoryImpl
25 |
26 | private val mockFlickrClient: FlickrClient = mock()
27 | private val mockPhotoDatabase: PhotoDao = mock()
28 |
29 | private val lat = "lat"
30 | private val lon = "lon"
31 | private val radius = "0.1"
32 | private val id1 = "id1"
33 | private val id2 = "id2"
34 | private val secret = "secret"
35 | private val server = "server"
36 | private val farm = "farm"
37 | private val photoResponseEntity1 = PhotoResponseEntity(id1, secret, server, farm)
38 | private val photoEntity1 = photoResponseEntity1.toPhotoEntity()
39 | private val photoResponseEntity2 = PhotoResponseEntity(id2, secret, server, farm)
40 | private val photoEntity2 = photoResponseEntity2.toPhotoEntity()
41 |
42 | @Before
43 | fun setUp() {
44 | repository = PhotoRepositoryImpl(mockFlickrClient, mockPhotoDatabase)
45 | }
46 |
47 | @Test
48 | fun `given empty database, when searchPhotoByLocation, then return the first photo`() {
49 | runBlocking {
50 | val photosFromDb = arrayOf()
51 | whenever(mockPhotoDatabase.selectAllPhotos()).thenReturn(photosFromDb)
52 |
53 | val photosFromFlickr = listOf(photoResponseEntity1)
54 | val result = Result.Success(photosFromFlickr)
55 | whenever(mockFlickrClient.searchPhoto(lat, lon, radius)).thenReturn(result)
56 | whenever(mockPhotoDatabase.insert(photoEntity1)).thenReturn(Unit)
57 |
58 | val test = repository.searchPhotoByLocation(lat, lon)
59 |
60 | verify(mockFlickrClient).searchPhoto(lat, lon, radius)
61 | Assert.assertEquals(test, Result.Success(result.data[0].toDomainModel()))
62 | }
63 | }
64 |
65 | @Test
66 | fun `given filled database, when searchPhotoByLocation returns existing photo, then return next photo`() {
67 | runBlocking {
68 | val photosFromDb = arrayOf(photoEntity1)
69 | whenever(mockPhotoDatabase.selectAllPhotos()).thenReturn(photosFromDb)
70 |
71 | val photosFromFlickr = listOf(photoResponseEntity1, photoResponseEntity2)
72 | val result = Result.Success(photosFromFlickr)
73 | whenever(mockFlickrClient.searchPhoto(lat, lon, radius)).thenReturn(result)
74 | whenever(mockPhotoDatabase.insert(photoEntity2)).thenReturn(Unit)
75 |
76 | val test = repository.searchPhotoByLocation(lat, lon)
77 |
78 | verify(mockFlickrClient).searchPhoto(lat, lon, radius)
79 | Assert.assertEquals(test, Result.Success(result.data[1].toDomainModel()))
80 | }
81 | }
82 |
83 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/domain/RetrievePhotosUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain
2 |
3 | import org.junit.Test
4 | import org.junit.Before
5 | import org.junit.Assert.assertEquals
6 |
7 | import java.io.IOException
8 |
9 | import kotlinx.coroutines.runBlocking
10 |
11 | import com.nhaarman.mockitokotlin2.mock
12 | import com.nhaarman.mockitokotlin2.verify
13 | import com.nhaarman.mockitokotlin2.whenever
14 |
15 | import com.gts.trackmypath.common.Result
16 | import com.gts.trackmypath.domain.model.Photo
17 | import com.gts.trackmypath.domain.usecase.RetrievePhotosUseCase
18 |
19 | class RetrievePhotosUseCaseTest {
20 |
21 | private lateinit var retrievePhotosUseCase: RetrievePhotosUseCase
22 | private val mockPhotoRepository: PhotoRepository = mock()
23 | private val photo = Photo("id", "secret", "server", "farm")
24 |
25 | @Before
26 | fun setUp() {
27 | retrievePhotosUseCase =
28 | RetrievePhotosUseCase(
29 | mockPhotoRepository
30 | )
31 | }
32 |
33 | @Test
34 | fun `when repository succeeds to retrieve photos then retrieve photos usecase returns success with photos`() {
35 | runBlocking {
36 | // given
37 | val expected = Result.Success(listOf(photo))
38 | whenever(mockPhotoRepository.loadAllPhotos()).thenReturn(expected)
39 | // when
40 | val result = retrievePhotosUseCase.invoke()
41 | // then
42 | verify(mockPhotoRepository).loadAllPhotos()
43 | assertEquals(expected, result)
44 | }
45 | }
46 |
47 | @Test
48 | fun `when repository fails to retrieve photos then retrieve photos usecase returns error`() {
49 | runBlocking {
50 | // given
51 | val expected = Result.Error(IOException("Failed to retrieve photos from database"))
52 | whenever(mockPhotoRepository.loadAllPhotos()).thenReturn(expected)
53 | // when
54 | val result = retrievePhotosUseCase.invoke()
55 | // then
56 | verify(mockPhotoRepository).loadAllPhotos()
57 | assertEquals(expected, result)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/domain/SearchPhotoByLocationUseCaseTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.domain
2 |
3 | import org.junit.Before
4 | import org.junit.Test
5 | import org.junit.Assert.assertEquals
6 |
7 | import java.io.IOException
8 |
9 | import kotlinx.coroutines.runBlocking
10 |
11 | import com.nhaarman.mockitokotlin2.mock
12 | import com.nhaarman.mockitokotlin2.verify
13 | import com.nhaarman.mockitokotlin2.whenever
14 |
15 | import com.gts.trackmypath.common.Result
16 | import com.gts.trackmypath.domain.model.Photo
17 | import com.gts.trackmypath.domain.usecase.SearchPhotoByLocationUseCase
18 |
19 | class SearchPhotoByLocationUseCaseTest {
20 |
21 | private lateinit var searchPhotoByLocationUseCase: SearchPhotoByLocationUseCase
22 | private val mockPhotoRepository: PhotoRepository = mock()
23 | private val lat = 1.0
24 | private val lon = 1.0
25 | private val photo = Photo("id", "secret", "server", "farm")
26 |
27 | @Before
28 | fun setUp() {
29 | searchPhotoByLocationUseCase =
30 | SearchPhotoByLocationUseCase(
31 | mockPhotoRepository
32 | )
33 | }
34 |
35 | @Test
36 | fun `searchPhotoByLocation get success`() {
37 | runBlocking {
38 | // given
39 | val expected = Result.Success(photo)
40 | whenever(mockPhotoRepository.searchPhotoByLocation(lat.toString(), lon.toString())).thenReturn(expected)
41 | // when
42 | val result = searchPhotoByLocationUseCase.invoke(lat, lon)
43 | // then
44 | verify(mockPhotoRepository).searchPhotoByLocation(lat.toString(), lon.toString())
45 | assertEquals(expected, result)
46 | }
47 | }
48 |
49 | @Test
50 | fun `searchPhotoByLocation get error`() {
51 | runBlocking {
52 | // given
53 | val expected = Result.Error(IOException("searchPhotoByLocation response error"))
54 | whenever(mockPhotoRepository.searchPhotoByLocation(lat.toString(), lon.toString())).thenReturn(expected)
55 | // when
56 | val result = searchPhotoByLocationUseCase.invoke(lat, lon)
57 | // then
58 | verify(mockPhotoRepository).searchPhotoByLocation(lat.toString(), lon.toString())
59 | assertEquals(expected, result)
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/presentation/LiveDataTestUtil.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import androidx.lifecycle.Observer
4 | import androidx.lifecycle.LiveData
5 |
6 | import java.util.concurrent.TimeUnit
7 | import java.util.concurrent.CountDownLatch
8 |
9 | /**
10 | * Safely handles observables from LiveData for testing.
11 | */
12 | object LiveDataTestUtil {
13 |
14 | /**
15 | * Gets the value of a LiveData safely.
16 | */
17 | @Throws(InterruptedException::class)
18 | fun getValue(liveData: LiveData): T? {
19 | var data: T? = null
20 | val latch = CountDownLatch(1)
21 | val observer = object : Observer {
22 | override fun onChanged(o: T?) {
23 | data = o
24 | latch.countDown()
25 | liveData.removeObserver(this)
26 | }
27 | }
28 | liveData.observeForever(observer)
29 | latch.await(2, TimeUnit.SECONDS)
30 |
31 | return data
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/presentation/PhotoStreamViewModelTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import org.junit.Test
4 | import org.junit.Rule
5 | import org.junit.Before
6 | import org.junit.Assert
7 |
8 | import androidx.arch.core.executor.testing.InstantTaskExecutorRule
9 |
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.runBlocking
12 | import kotlinx.coroutines.test.setMain
13 | import kotlinx.coroutines.ExperimentalCoroutinesApi
14 |
15 | import com.nhaarman.mockitokotlin2.mock
16 | import com.nhaarman.mockitokotlin2.whenever
17 |
18 | import com.gts.trackmypath.common.Result
19 | import com.gts.trackmypath.domain.model.Photo
20 | import com.gts.trackmypath.domain.usecase.RetrievePhotosUseCase
21 | import com.gts.trackmypath.presentation.model.PhotoViewItem
22 | import com.gts.trackmypath.presentation.model.toPresentationModel
23 |
24 | class PhotoStreamViewModelTest {
25 |
26 | @get:Rule
27 | var instantTaskExecutorRule = InstantTaskExecutorRule()
28 |
29 | private lateinit var viewModel: PhotoStreamViewModel
30 | private val mockRetrievePhotosUseCase: RetrievePhotosUseCase = mock()
31 |
32 | @ExperimentalCoroutinesApi
33 | @Before
34 | fun setUp() {
35 | Dispatchers.setMain(Dispatchers.Unconfined)
36 | viewModel = PhotoStreamViewModel(mockRetrievePhotosUseCase)
37 | }
38 |
39 | @Test
40 | fun `given a photo, when view model retrieves photo, then returns a list with the photo item`() {
41 | val photo = Photo("id", "secret", "server", "farm")
42 | val expected = Result.Success(listOf(photo))
43 |
44 | runBlocking {
45 | whenever(mockRetrievePhotosUseCase.invoke()).thenReturn(expected)
46 |
47 | viewModel.retrievePhotos()
48 |
49 | val photoList: List? =
50 | LiveDataTestUtil.getValue(viewModel.photosByLocation)
51 | Assert.assertNotNull(photoList)
52 | Assert.assertEquals(expected.data.map { it.toPresentationModel() }, photoList)
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/src/test/java/com/gts/trackmypath/presentation/UtilsTest.kt:
--------------------------------------------------------------------------------
1 | package com.gts.trackmypath.presentation
2 |
3 | import org.junit.Test
4 | import org.junit.Assert.assertEquals
5 |
6 | class UtilsTest {
7 |
8 | @Test
9 | fun `buildUri returns right uri`() {
10 | val expected = "https://farm01.staticflickr.com/10/101_xxx.jpg"
11 | // when
12 | val farm = "01"
13 | val server = "10"
14 | val id = "101"
15 | val secret = "xxx"
16 | val result = buildUri(farm, server, id, secret)
17 | // then
18 | assertEquals(expected, result)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | kotlin_version = '1.4.21'
6 | appcompat_version = '1.2.0'
7 | playServicesLocation_version = '17.1.0'
8 | material_version = '1.2.1'
9 | ktx_version = '1.5.0-alpha05'
10 | constraintLayout_version = '2.0.4'
11 | recyclerview_version = '28.0.0'
12 | lifecycle_version = '2.2.0'
13 | localBroadcastManager_version = '1.0.0'
14 | preference_ktx_version = '1.1.1'
15 | room_version = '2.2.6'
16 | koin_version = '2.1.6'
17 | coroutines_version = '1.3.9'
18 | retrofit_version = '2.9.0'
19 | retrofitCoroutinesAdapter_version = '0.9.2'
20 | okHttp_version = '4.9.0'
21 | moshi_version = '1.11.0'
22 | moshiConverter_version = '2.9.0'
23 | fresco_version = '2.0.0'
24 | timber_version = '4.7.1'
25 | }
26 |
27 | repositories {
28 | google()
29 | jcenter()
30 | gradlePluginPortal()
31 | }
32 |
33 | dependencies {
34 | classpath 'com.android.tools.build:gradle:4.1.1'
35 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
36 | classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.15.0"
37 | classpath "io.gitlab.arturbosch.detekt:detekt-cli:1.15.0"
38 | // NOTE: Do not place your application dependencies here; they belong
39 | // in the individual module build.gradle files
40 | }
41 |
42 | }
43 |
44 | allprojects {
45 | repositories {
46 | google()
47 | jcenter()
48 | }
49 | }
50 |
51 | task clean(type: Delete) {
52 | delete rootProject.buildDir
53 | }
54 |
--------------------------------------------------------------------------------
/config/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | build:
2 | maxIssues: 0
3 | excludeCorrectable: false
4 | weights:
5 | # complexity: 2
6 | # LongParameterList: 1
7 | # style: 1
8 | # comments: 1
9 |
10 | config:
11 | validation: true
12 | warningsAsErrors: false
13 | # when writing own rules with new properties, exclude the property path e.g.: 'my_rule_set,.*>.*>[my_property]'
14 | excludes: ''
15 |
16 | processors:
17 | active: true
18 | exclude:
19 | - 'DetektProgressListener'
20 | # - 'FunctionCountProcessor'
21 | # - 'PropertyCountProcessor'
22 | # - 'ClassCountProcessor'
23 | # - 'PackageCountProcessor'
24 | # - 'KtFileCountProcessor'
25 |
26 | console-reports:
27 | active: true
28 | exclude:
29 | - 'ProjectStatisticsReport'
30 | - 'ComplexityReport'
31 | - 'NotificationReport'
32 | # - 'FindingsReport'
33 | - 'FileBasedFindingsReport'
34 |
35 | output-reports:
36 | active: true
37 | exclude:
38 | # - 'TxtOutputReport'
39 | # - 'XmlOutputReport'
40 | # - 'HtmlOutputReport'
41 |
42 | comments:
43 | active: true
44 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
45 | AbsentOrWrongFileLicense:
46 | active: false
47 | licenseTemplateFile: 'license.template'
48 | CommentOverPrivateFunction:
49 | active: false
50 | CommentOverPrivateProperty:
51 | active: false
52 | EndOfSentenceFormat:
53 | active: false
54 | endOfSentenceFormat: '([.?!][ \t\n\r\f<])|([.?!:]$)'
55 | UndocumentedPublicClass:
56 | active: false
57 | searchInNestedClass: true
58 | searchInInnerClass: true
59 | searchInInnerObject: true
60 | searchInInnerInterface: true
61 | UndocumentedPublicFunction:
62 | active: false
63 | UndocumentedPublicProperty:
64 | active: false
65 |
66 | complexity:
67 | active: true
68 | ComplexCondition:
69 | active: true
70 | threshold: 4
71 | ComplexInterface:
72 | active: false
73 | threshold: 10
74 | includeStaticDeclarations: false
75 | includePrivateDeclarations: false
76 | ComplexMethod:
77 | active: true
78 | threshold: 15
79 | ignoreSingleWhenExpression: false
80 | ignoreSimpleWhenEntries: false
81 | ignoreNestingFunctions: false
82 | nestingFunctions: [run, let, apply, with, also, use, forEach, isNotNull, ifNull]
83 | LabeledExpression:
84 | active: false
85 | ignoredLabels: []
86 | LargeClass:
87 | active: true
88 | threshold: 600
89 | LongMethod:
90 | active: true
91 | threshold: 60
92 | LongParameterList:
93 | active: true
94 | functionThreshold: 6
95 | constructorThreshold: 7
96 | ignoreDefaultParameters: false
97 | ignoreDataClasses: true
98 | ignoreAnnotated: []
99 | MethodOverloading:
100 | active: false
101 | threshold: 6
102 | NamedArguments:
103 | active: false
104 | threshold: 3
105 | NestedBlockDepth:
106 | active: true
107 | threshold: 4
108 | ReplaceSafeCallChainWithRun:
109 | active: false
110 | StringLiteralDuplication:
111 | active: false
112 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
113 | threshold: 3
114 | ignoreAnnotation: true
115 | excludeStringsWithLessThan5Characters: true
116 | ignoreStringsRegex: '$^'
117 | TooManyFunctions:
118 | active: true
119 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
120 | thresholdInFiles: 14
121 | thresholdInClasses: 14
122 | thresholdInInterfaces: 11
123 | thresholdInObjects: 11
124 | thresholdInEnums: 11
125 | ignoreDeprecated: false
126 | ignorePrivate: false
127 | ignoreOverridden: false
128 |
129 | coroutines:
130 | active: true
131 | GlobalCoroutineUsage:
132 | active: false
133 | RedundantSuspendModifier:
134 | active: false
135 | SuspendFunWithFlowReturnType:
136 | active: false
137 |
138 | empty-blocks:
139 | active: true
140 | EmptyCatchBlock:
141 | active: true
142 | allowedExceptionNameRegex: '_|(ignore|expected).*'
143 | EmptyClassBlock:
144 | active: true
145 | EmptyDefaultConstructor:
146 | active: true
147 | EmptyDoWhileBlock:
148 | active: true
149 | EmptyElseBlock:
150 | active: true
151 | EmptyFinallyBlock:
152 | active: true
153 | EmptyForBlock:
154 | active: true
155 | EmptyFunctionBlock:
156 | active: true
157 | ignoreOverridden: false
158 | EmptyIfBlock:
159 | active: true
160 | EmptyInitBlock:
161 | active: true
162 | EmptyKtFile:
163 | active: true
164 | EmptySecondaryConstructor:
165 | active: true
166 | EmptyTryBlock:
167 | active: true
168 | EmptyWhenBlock:
169 | active: true
170 | EmptyWhileBlock:
171 | active: true
172 |
173 | exceptions:
174 | active: true
175 | ExceptionRaisedInUnexpectedLocation:
176 | active: false
177 | methodNames: [toString, hashCode, equals, finalize]
178 | InstanceOfCheckForException:
179 | active: false
180 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
181 | NotImplementedDeclaration:
182 | active: false
183 | PrintStackTrace:
184 | active: false
185 | RethrowCaughtException:
186 | active: false
187 | ReturnFromFinally:
188 | active: false
189 | ignoreLabeled: false
190 | SwallowedException:
191 | active: false
192 | ignoredExceptionTypes:
193 | - InterruptedException
194 | - NumberFormatException
195 | - ParseException
196 | - MalformedURLException
197 | allowedExceptionNameRegex: '_|(ignore|expected).*'
198 | ThrowingExceptionFromFinally:
199 | active: false
200 | ThrowingExceptionInMain:
201 | active: false
202 | ThrowingExceptionsWithoutMessageOrCause:
203 | active: false
204 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
205 | exceptions:
206 | - IllegalArgumentException
207 | - IllegalStateException
208 | - IOException
209 | ThrowingNewInstanceOfSameException:
210 | active: false
211 | TooGenericExceptionCaught:
212 | active: true
213 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
214 | exceptionNames:
215 | - ArrayIndexOutOfBoundsException
216 | - Error
217 | - Exception
218 | - IllegalMonitorStateException
219 | - NullPointerException
220 | - IndexOutOfBoundsException
221 | - RuntimeException
222 | - Throwable
223 | allowedExceptionNameRegex: '_|(ignore|expected).*'
224 | TooGenericExceptionThrown:
225 | active: true
226 | exceptionNames:
227 | - Error
228 | - Exception
229 | - Throwable
230 | - RuntimeException
231 |
232 | formatting:
233 | active: true
234 | android: false
235 | autoCorrect: true
236 | AnnotationOnSeparateLine:
237 | active: false
238 | autoCorrect: true
239 | AnnotationSpacing:
240 | active: false
241 | autoCorrect: true
242 | ArgumentListWrapping:
243 | active: false
244 | autoCorrect: true
245 | ChainWrapping:
246 | active: true
247 | autoCorrect: true
248 | CommentSpacing:
249 | active: true
250 | autoCorrect: true
251 | EnumEntryNameCase:
252 | active: false
253 | autoCorrect: true
254 | Filename:
255 | active: true
256 | FinalNewline:
257 | active: true
258 | autoCorrect: true
259 | insertFinalNewLine: true
260 | ImportOrdering:
261 | active: false
262 | autoCorrect: true
263 | layout: 'idea'
264 | Indentation:
265 | active: false
266 | autoCorrect: true
267 | indentSize: 4
268 | continuationIndentSize: 4
269 | MaximumLineLength:
270 | active: true
271 | maxLineLength: 120
272 | ModifierOrdering:
273 | active: true
274 | autoCorrect: true
275 | MultiLineIfElse:
276 | active: true
277 | autoCorrect: true
278 | NoBlankLineBeforeRbrace:
279 | active: true
280 | autoCorrect: true
281 | NoConsecutiveBlankLines:
282 | active: true
283 | autoCorrect: true
284 | NoEmptyClassBody:
285 | active: true
286 | autoCorrect: true
287 | NoEmptyFirstLineInMethodBlock:
288 | active: false
289 | autoCorrect: true
290 | NoLineBreakAfterElse:
291 | active: true
292 | autoCorrect: true
293 | NoLineBreakBeforeAssignment:
294 | active: true
295 | autoCorrect: true
296 | NoMultipleSpaces:
297 | active: true
298 | autoCorrect: true
299 | NoSemicolons:
300 | active: true
301 | autoCorrect: true
302 | NoTrailingSpaces:
303 | active: true
304 | autoCorrect: true
305 | NoUnitReturn:
306 | active: true
307 | autoCorrect: true
308 | NoUnusedImports:
309 | active: true
310 | autoCorrect: true
311 | NoWildcardImports:
312 | active: true
313 | PackageName:
314 | active: true
315 | autoCorrect: true
316 | ParameterListWrapping:
317 | active: true
318 | autoCorrect: true
319 | indentSize: 4
320 | SpacingAroundColon:
321 | active: true
322 | autoCorrect: true
323 | SpacingAroundComma:
324 | active: true
325 | autoCorrect: true
326 | SpacingAroundCurly:
327 | active: true
328 | autoCorrect: true
329 | SpacingAroundDot:
330 | active: true
331 | autoCorrect: true
332 | SpacingAroundDoubleColon:
333 | active: false
334 | autoCorrect: true
335 | SpacingAroundKeyword:
336 | active: true
337 | autoCorrect: true
338 | SpacingAroundOperators:
339 | active: true
340 | autoCorrect: true
341 | SpacingAroundParens:
342 | active: true
343 | autoCorrect: true
344 | SpacingAroundRangeOperator:
345 | active: true
346 | autoCorrect: true
347 | SpacingBetweenDeclarationsWithAnnotations:
348 | active: false
349 | autoCorrect: true
350 | SpacingBetweenDeclarationsWithComments:
351 | active: false
352 | autoCorrect: true
353 | StringTemplate:
354 | active: true
355 | autoCorrect: true
356 |
357 | naming:
358 | active: true
359 | ClassNaming:
360 | active: true
361 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
362 | classPattern: '[A-Z][a-zA-Z0-9]*'
363 | ConstructorParameterNaming:
364 | active: true
365 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
366 | parameterPattern: '[a-z][A-Za-z0-9]*'
367 | privateParameterPattern: '[a-z][A-Za-z0-9]*'
368 | excludeClassPattern: '$^'
369 | ignoreOverridden: true
370 | EnumNaming:
371 | active: true
372 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
373 | enumEntryPattern: '[A-Z][_a-zA-Z0-9]*'
374 | ForbiddenClassName:
375 | active: false
376 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
377 | forbiddenName: []
378 | FunctionMaxLength:
379 | active: false
380 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
381 | maximumFunctionNameLength: 30
382 | FunctionMinLength:
383 | active: false
384 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
385 | minimumFunctionNameLength: 3
386 | FunctionNaming:
387 | active: true
388 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
389 | functionPattern: '([a-z][a-zA-Z0-9]*)|(`.*`)'
390 | excludeClassPattern: '$^'
391 | ignoreOverridden: true
392 | ignoreAnnotated: ['Composable']
393 | FunctionParameterNaming:
394 | active: true
395 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
396 | parameterPattern: '[a-z][A-Za-z0-9]*'
397 | excludeClassPattern: '$^'
398 | ignoreOverridden: true
399 | InvalidPackageDeclaration:
400 | active: false
401 | rootPackage: ''
402 | MatchingDeclarationName:
403 | active: true
404 | mustBeFirst: true
405 | MemberNameEqualsClassName:
406 | active: true
407 | ignoreOverridden: true
408 | NonBooleanPropertyPrefixedWithIs:
409 | active: false
410 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
411 | ObjectPropertyNaming:
412 | active: true
413 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
414 | constantPattern: '[A-Za-z][_A-Za-z0-9]*'
415 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
416 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*'
417 | PackageNaming:
418 | active: true
419 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
420 | packagePattern: '[a-z]+(\.[a-z][A-Za-z0-9]*)*'
421 | TopLevelPropertyNaming:
422 | active: true
423 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
424 | constantPattern: '[A-Z][_A-Z0-9]*'
425 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*'
426 | privatePropertyPattern: '_?[A-Za-z][_A-Za-z0-9]*'
427 | VariableMaxLength:
428 | active: false
429 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
430 | maximumVariableNameLength: 64
431 | VariableMinLength:
432 | active: false
433 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
434 | minimumVariableNameLength: 1
435 | VariableNaming:
436 | active: true
437 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
438 | variablePattern: '[a-z][A-Za-z0-9]*'
439 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*'
440 | excludeClassPattern: '$^'
441 | ignoreOverridden: true
442 |
443 | performance:
444 | active: true
445 | ArrayPrimitive:
446 | active: true
447 | ForEachOnRange:
448 | active: true
449 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
450 | SpreadOperator:
451 | active: true
452 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
453 | UnnecessaryTemporaryInstantiation:
454 | active: true
455 |
456 | potential-bugs:
457 | active: true
458 | Deprecation:
459 | active: false
460 | DuplicateCaseInWhenExpression:
461 | active: true
462 | EqualsAlwaysReturnsTrueOrFalse:
463 | active: true
464 | EqualsWithHashCodeExist:
465 | active: true
466 | ExplicitGarbageCollectionCall:
467 | active: true
468 | HasPlatformType:
469 | active: false
470 | IgnoredReturnValue:
471 | active: false
472 | restrictToAnnotatedMethods: true
473 | returnValueAnnotations: ['*.CheckReturnValue', '*.CheckResult']
474 | ImplicitDefaultLocale:
475 | active: false
476 | ImplicitUnitReturnType:
477 | active: false
478 | allowExplicitReturnType: true
479 | InvalidRange:
480 | active: true
481 | IteratorHasNextCallsNextMethod:
482 | active: true
483 | IteratorNotThrowingNoSuchElementException:
484 | active: true
485 | LateinitUsage:
486 | active: false
487 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
488 | excludeAnnotatedProperties: []
489 | ignoreOnClassesPattern: ''
490 | MapGetWithNotNullAssertionOperator:
491 | active: false
492 | MissingWhenCase:
493 | active: true
494 | allowElseExpression: true
495 | NullableToStringCall:
496 | active: false
497 | RedundantElseInWhen:
498 | active: true
499 | UnconditionalJumpStatementInLoop:
500 | active: false
501 | UnnecessaryNotNullOperator:
502 | active: false
503 | UnnecessarySafeCall:
504 | active: false
505 | UnreachableCode:
506 | active: true
507 | UnsafeCallOnNullableType:
508 | active: true
509 | UnsafeCast:
510 | active: false
511 | UselessPostfixExpression:
512 | active: false
513 | WrongEqualsTypeParameter:
514 | active: true
515 |
516 | style:
517 | active: true
518 | ClassOrdering:
519 | active: false
520 | CollapsibleIfStatements:
521 | active: false
522 | DataClassContainsFunctions:
523 | active: false
524 | conversionFunctionPrefix: 'to'
525 | DataClassShouldBeImmutable:
526 | active: false
527 | EqualsNullCall:
528 | active: true
529 | EqualsOnSignatureLine:
530 | active: false
531 | ExplicitCollectionElementAccessMethod:
532 | active: false
533 | ExplicitItLambdaParameter:
534 | active: false
535 | ExpressionBodySyntax:
536 | active: false
537 | includeLineWrapping: false
538 | ForbiddenComment:
539 | active: true
540 | values: ['TODO:', 'FIXME:', 'STOPSHIP:']
541 | allowedPatterns: ''
542 | ForbiddenImport:
543 | active: false
544 | imports: []
545 | forbiddenPatterns: ''
546 | ForbiddenMethodCall:
547 | active: false
548 | methods: ['kotlin.io.println', 'kotlin.io.print']
549 | ForbiddenPublicDataClass:
550 | active: false
551 | ignorePackages: ['*.internal', '*.internal.*']
552 | ForbiddenVoid:
553 | active: false
554 | ignoreOverridden: false
555 | ignoreUsageInGenerics: false
556 | FunctionOnlyReturningConstant:
557 | active: true
558 | ignoreOverridableFunction: true
559 | excludedFunctions: 'describeContents'
560 | excludeAnnotatedFunction: ['dagger.Provides']
561 | LibraryCodeMustSpecifyReturnType:
562 | active: true
563 | LibraryEntitiesShouldNotBePublic:
564 | active: false
565 | LoopWithTooManyJumpStatements:
566 | active: true
567 | maxJumpCount: 1
568 | MagicNumber:
569 | active: true
570 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
571 | ignoreNumbers: ['-1', '0', '1', '2']
572 | ignoreHashCodeFunction: true
573 | ignorePropertyDeclaration: false
574 | ignoreLocalVariableDeclaration: false
575 | ignoreConstantDeclaration: true
576 | ignoreCompanionObjectPropertyDeclaration: true
577 | ignoreAnnotation: false
578 | ignoreNamedArgument: true
579 | ignoreEnums: false
580 | ignoreRanges: false
581 | MandatoryBracesIfStatements:
582 | active: false
583 | MandatoryBracesLoops:
584 | active: false
585 | MaxLineLength:
586 | active: true
587 | maxLineLength: 120
588 | excludePackageStatements: true
589 | excludeImportStatements: true
590 | excludeCommentStatements: false
591 | MayBeConst:
592 | active: true
593 | ModifierOrder:
594 | active: true
595 | NestedClassesVisibility:
596 | active: false
597 | NewLineAtEndOfFile:
598 | active: true
599 | NoTabs:
600 | active: false
601 | OptionalAbstractKeyword:
602 | active: true
603 | OptionalUnit:
604 | active: false
605 | OptionalWhenBraces:
606 | active: false
607 | PreferToOverPairSyntax:
608 | active: false
609 | ProtectedMemberInFinalClass:
610 | active: true
611 | RedundantExplicitType:
612 | active: false
613 | RedundantHigherOrderMapUsage:
614 | active: false
615 | RedundantVisibilityModifierRule:
616 | active: false
617 | ReturnCount:
618 | active: true
619 | max: 2
620 | excludedFunctions: 'equals'
621 | excludeLabeled: false
622 | excludeReturnFromLambda: true
623 | excludeGuardClauses: false
624 | SafeCast:
625 | active: true
626 | SerialVersionUIDInSerializableClass:
627 | active: false
628 | SpacingBetweenPackageAndImports:
629 | active: false
630 | ThrowsCount:
631 | active: true
632 | max: 2
633 | TrailingWhitespace:
634 | active: false
635 | UnderscoresInNumericLiterals:
636 | active: false
637 | acceptableDecimalLength: 5
638 | UnnecessaryAbstractClass:
639 | active: true
640 | excludeAnnotatedClasses: ['dagger.Module']
641 | UnnecessaryAnnotationUseSiteTarget:
642 | active: false
643 | UnnecessaryApply:
644 | active: false
645 | UnnecessaryInheritance:
646 | active: true
647 | UnnecessaryLet:
648 | active: false
649 | UnnecessaryParentheses:
650 | active: false
651 | UntilInsteadOfRangeTo:
652 | active: false
653 | UnusedImports:
654 | active: false
655 | UnusedPrivateClass:
656 | active: true
657 | UnusedPrivateMember:
658 | active: false
659 | allowedNames: '(_|ignored|expected|serialVersionUID)'
660 | UseArrayLiteralsInAnnotations:
661 | active: false
662 | UseCheckNotNull:
663 | active: false
664 | UseCheckOrError:
665 | active: false
666 | UseDataClass:
667 | active: false
668 | excludeAnnotatedClasses: []
669 | allowVars: false
670 | UseEmptyCounterpart:
671 | active: false
672 | UseIfEmptyOrIfBlank:
673 | active: false
674 | UseIfInsteadOfWhen:
675 | active: false
676 | UseRequire:
677 | active: false
678 | UseRequireNotNull:
679 | active: false
680 | UselessCallOnNotNull:
681 | active: true
682 | UtilityClassWithPublicConstructor:
683 | active: true
684 | VarCouldBeVal:
685 | active: false
686 | WildcardImport:
687 | active: true
688 | excludes: ['**/test/**', '**/androidTest/**', '**/commonTest/**', '**/jvmTest/**', '**/jsTest/**', '**/iosTest/**']
689 | excludeImports: ['java.util.*', 'kotlinx.android.synthetic.*']
690 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Mar 01 18:06:48 CET 2020
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-6.6-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 |
--------------------------------------------------------------------------------
/screenshots/scrn1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/screenshots/scrn1.png
--------------------------------------------------------------------------------
/screenshots/summary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gs-ts/TrackMyPath/52b11c0dc497dd873d1213c2c9df355fa163f893/screenshots/summary.png
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------