diff --git a/.gitignore b/.gitignore
index 5f712c90..d7b08dcb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -110,6 +110,8 @@ screenshots
store
api_aqicn.xml
+# C++ generated files
+app/.cxx/*
app/.cxx
mainframer
remoteAssemble
diff --git a/app/build.gradle b/app/build.gradle
index 5f585505..a1f69c78 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -1,6 +1,14 @@
apply plugin: 'com.android.application'
+apply plugin: 'com.livinglifetechway.quickpermissions_plugin'
+apply plugin: 'io.fabric'
+apply plugin: "kotlin-android"
+apply plugin: "kotlin-android-extensions"
+apply plugin: "kotlin-kapt"
+apply plugin: 'dagger.hilt.android.plugin'
+apply plugin: "de.mannodermaus.android-junit5"
apply plugin: 'com.google.firebase.crashlytics'
+
android {
compileSdkVersion project.mCompileSdkVersion.toInteger()
defaultConfig {
@@ -16,6 +24,7 @@ android {
}
}
}
+ buildFeatures { dataBinding = true }
buildTypes {
release {
minifyEnabled false
@@ -34,8 +43,12 @@ android {
}
}
compileOptions {
- targetCompatibility 1.8
- sourceCompatibility 1.8
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_1_8.toString()
}
testOptions {
@@ -54,13 +67,17 @@ dependencies {
implementation 'com.karumi:dexter:6.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
- implementation 'androidx.cardview:cardview:1.0.0'
+ implementation 'androidx.cardview:cardview:1.0.0
+ implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'com.google.android.material:material:1.3.0'
+
implementation "androidx.preference:preference:1.1.1"
implementation 'com.takisoft.preferencex:preferencex:1.1.0-alpha05'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.jakewharton:butterknife:10.2.0'
+ implementation 'androidx.appcompat:appcompat:1.1.0'
+ implementation 'androidx.legacy:legacy-support-v4:1.0.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
implementation 'com.jakewharton.rxbinding2:rxbinding:2.2.0'
@@ -90,6 +107,66 @@ dependencies {
implementation 'com.github.vic797:prowebview:2.2.1'
+ // Firebase
+ implementation platform('com.google.firebase:firebase-bom:26.0.0')
+ implementation 'com.google.firebase:firebase-database-ktx'
+
+ // Dexter Permissions Requesting
+ implementation 'com.karumi:dexter:6.2.1'
+
+ // Android KTX
+ implementation "androidx.fragment:fragment-ktx:1.2.5"
+ implementation "androidx.activity:activity-ktx:1.1.0"
+ implementation "androidx.fragment:fragment:1.3.0-beta01"
+
+ // Retrofit
+ implementation 'com.squareup.retrofit2:retrofit:2.6.1'
+ implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
+
+ // Debug Retrofit
+ implementation("com.squareup.okhttp3:logging-interceptor:4.7.2")
+
+ // Hilt
+ implementation "com.google.dagger:hilt-android:2.28-alpha"
+ kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
+ implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
+ kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
+
+ // ViewModel
+ implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
+
+ // LiveData
+ implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0"
+
+ // Jetpack Navigation
+ implementation 'androidx.navigation:navigation-fragment-ktx:2.3.1'
+ implementation 'androidx.navigation:navigation-ui-ktx:2.3.1'
+
+ // Coroutines
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7'
+ implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7'
+ testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.7'
+
+ // Junit 5
+ testImplementation "org.junit.jupiter:junit-jupiter-api:5.6.2"
+ testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.6.2"
+
+ // Mockk
+ testImplementation "io.mockk:mockk:1.10.0"
+
+ // OkHttp Mock Web Server
+ testImplementation "com.squareup.okhttp3:mockwebserver:4.7.2"
+
+ testImplementation 'junit:junit:4.12'
+ testImplementation 'androidx.test:core:1.3.0'
+ testImplementation 'org.mockito:mockito-core:1.10.19'
+ androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
+ testImplementation "android.arch.core:core-testing:1.1.1"
+ androidTestImplementation 'androidx.test:runner:1.3.0'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+
+ implementation 'com.android.support:design:28.0.0'
+
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
@@ -98,7 +175,9 @@ dependencies {
testImplementation 'org.mockito:mockito-core:3.3.3'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
+
}
apply plugin: 'com.google.gms.google-services'
-
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
diff --git a/app/src/androidTest/java/hpsaturn/pollutionreporter/ExampleInstrumentedTest.java b/app/src/androidTest/java/hpsaturn/pollutionreporter/ExampleInstrumentedTest.java
deleted file mode 100644
index 8cccceb8..00000000
--- a/app/src/androidTest/java/hpsaturn/pollutionreporter/ExampleInstrumentedTest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package hpsaturn.pollutionreporter;
-
-import android.content.Context;
-import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import static org.junit.Assert.*;
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * @see Testing documentation
- */
-@RunWith(AndroidJUnit4.class)
-public class ExampleInstrumentedTest {
- @Test
- public void useAppContext() {
- // Context of the app under test.
- Context appContext = InstrumentationRegistry.getTargetContext();
-
- assertEquals("hpsaturn.pollutionreporter", appContext.getPackageName());
- }
-}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dd8676c3..b1671d01 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,6 +21,7 @@
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
+
+
+
+
-
-
-
-
-
-
+ android:exported="true" />
+
+
+
+
-
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/AppData.java b/app/src/main/java/hpsaturn/pollutionreporter/AppData.java
index 07855571..386dbd03 100644
--- a/app/src/main/java/hpsaturn/pollutionreporter/AppData.java
+++ b/app/src/main/java/hpsaturn/pollutionreporter/AppData.java
@@ -8,11 +8,14 @@
import com.polidea.rxandroidble2.RxBleClient;
import com.polidea.rxandroidble2.internal.RxBleLog;
+import dagger.hilt.android.HiltAndroidApp;
import hpsaturn.pollutionreporter.api.AqicnApiManager;
/**
* Created by Antonio Vanegas @hpsaturn on 6/13/18.
*/
+
+@HiltAndroidApp
public class AppData extends MultiDexApplication{
private RxBleClient rxBleClient;
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/core/data/mappers/Mapper.kt b/app/src/main/java/hpsaturn/pollutionreporter/core/data/mappers/Mapper.kt
new file mode 100644
index 00000000..6a68680e
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/core/data/mappers/Mapper.kt
@@ -0,0 +1,5 @@
+package hpsaturn.pollutionreporter.core.data.mappers
+
+interface Mapper {
+ operator fun invoke(input: I): O
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/core/domain/entities/Result.kt b/app/src/main/java/hpsaturn/pollutionreporter/core/domain/entities/Result.kt
new file mode 100644
index 00000000..096d6986
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/core/domain/entities/Result.kt
@@ -0,0 +1,13 @@
+package hpsaturn.pollutionreporter.core.domain.entities
+
+sealed class Result {
+ override fun toString(): String = when (this) {
+ is Success<*> -> "Success[data=$data]"
+ is ErrorResult -> "Error[exception=$exception]"
+ InProgress -> "Loading"
+ }
+}
+
+data class Success(val data: T) : Result()
+data class ErrorResult(val exception: Throwable) : Result()
+object InProgress : Result()
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/core/domain/errors/Exceptions.kt b/app/src/main/java/hpsaturn/pollutionreporter/core/domain/errors/Exceptions.kt
new file mode 100644
index 00000000..fe38c21f
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/core/domain/errors/Exceptions.kt
@@ -0,0 +1,7 @@
+package hpsaturn.pollutionreporter.core.domain.errors
+
+class ServerException(message: String = "") : Exception(message)
+class PermissionException(message: String = "") : Exception(message)
+class PermissionNotGrantedException(message: String = "") : Exception(message)
+class ConnectionException(message: String = "") : Exception(message)
+class UnexpectedException(message: String = "") : Exception(message)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardActivity.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardActivity.kt
new file mode 100644
index 00000000..4aff6d62
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardActivity.kt
@@ -0,0 +1,68 @@
+package hpsaturn.pollutionreporter.dashboard
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.util.Log
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.findNavController
+import androidx.navigation.ui.setupWithNavController
+import com.hpsaturn.tools.Logger
+import dagger.hilt.android.AndroidEntryPoint
+import hpsaturn.pollutionreporter.BaseActivity
+import hpsaturn.pollutionreporter.Config
+import hpsaturn.pollutionreporter.R
+import hpsaturn.pollutionreporter.service.RecordTrackScheduler
+import hpsaturn.pollutionreporter.service.RecordTrackService
+import kotlinx.android.synthetic.main.activity_dashboard.*
+
+private val TAG = DashboardActivity::class.java.simpleName
+
+@AndroidEntryPoint
+class DashboardActivity : AppCompatActivity() {
+
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_dashboard)
+
+ setupNavigationComponent()
+ startRecordTrackService()
+ checkBluetoothSupport()
+ }
+
+ /**
+ * Sets up the app to use Android Navigation Component. More information of this component can
+ * be found here: https://developer.android.com/guide/navigation/navigation-getting-started
+ */
+ private fun setupNavigationComponent() {
+ val navController = findNavController(R.id.nav_host_fragment)
+ bottomNavigation.setupWithNavController(navController)
+ }
+
+ private fun startRecordTrackService() {
+ Log.i(TAG, "starting RecordTrackService..")
+ val trackServiceIntent = Intent(this, RecordTrackService::class.java)
+ startService(trackServiceIntent)
+ RecordTrackScheduler.startScheduleService(this, Config.DEFAULT_INTERVAL)
+ }
+
+
+ // TODO (@juanpa097) - The code bellow this comment should be refactor.
+
+ private fun checkBluetoothSupport() { // Use this check to determine whether BLE is supported on the device.
+ val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ val mBluetoothAdapter = bluetoothManager.adapter
+ if (!packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
+ Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show()
+ } else if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled) {
+ val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
+ startActivityForResult(enableBtIntent, 0)
+ } else Logger.i(BaseActivity.TAG, "[BLE] checkBluetoohtBle: ready!")
+ }
+
+}
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardModule.kt
new file mode 100644
index 00000000..87003128
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/DashboardModule.kt
@@ -0,0 +1,46 @@
+package hpsaturn.pollutionreporter.dashboard
+
+import android.location.Location
+import androidx.lifecycle.LiveData
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.core.domain.entities.Result
+import hpsaturn.pollutionreporter.dashboard.data.mappers.AirQualityStatusMapper
+import hpsaturn.pollutionreporter.dashboard.data.models.AqicnFeedResponse
+import hpsaturn.pollutionreporter.dashboard.data.repositories.AirQualityStatusRepositoryImpl
+import hpsaturn.pollutionreporter.dashboard.data.services.AqicnApiFeedService
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import hpsaturn.pollutionreporter.dashboard.domain.repositories.AirQualityStatusRepository
+import hpsaturn.pollutionreporter.dashboard.presentation.CurrentLocationLiveData
+import retrofit2.Retrofit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ApplicationComponent::class)
+abstract class DashboardModule {
+
+ @Binds
+ abstract fun bindAirQualityStatusMapper(airQualityStatusMapper: AirQualityStatusMapper):
+ Mapper
+
+ @Binds
+ abstract fun bindAirQualityStatusRepositoryImpl(
+ airQualityStatusRepositoryImpl: AirQualityStatusRepositoryImpl
+ ): AirQualityStatusRepository
+
+ @Binds
+ abstract fun bindCurrentLocationLiveData(currentLocationLiveData: CurrentLocationLiveData):
+ LiveData>
+
+ companion object {
+ @Singleton
+ @Provides
+ fun provideAqicnApiFeedService(retrofit: Retrofit): AqicnApiFeedService =
+ retrofit.create(AqicnApiFeedService::class.java)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/mappers/AirQualityStatusMapper.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/mappers/AirQualityStatusMapper.kt
new file mode 100644
index 00000000..2214cc68
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/mappers/AirQualityStatusMapper.kt
@@ -0,0 +1,15 @@
+package hpsaturn.pollutionreporter.dashboard.data.mappers
+
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.dashboard.data.models.AqicnFeedResponse
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import javax.inject.Inject
+
+class AirQualityStatusMapper @Inject constructor() : Mapper {
+ override fun invoke(input: AqicnFeedResponse): AirQualityStatus = AirQualityStatus(
+ input.data.aqi,
+ input.data.city.name,
+ input.data.city.geo[0],
+ input.data.city.geo[1]
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/AqicnFeedResponse.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/AqicnFeedResponse.kt
new file mode 100644
index 00000000..8cf9f051
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/AqicnFeedResponse.kt
@@ -0,0 +1,13 @@
+package hpsaturn.pollutionreporter.dashboard.data.models
+
+
+/**
+ * Air quality information fetched from Aqicn API.
+ * @property data Air quality station data.
+ * @property status Status code, can be ok or error.
+ * See more here: https://aqicn.org/json-api/doc/
+ */
+data class AqicnFeedResponse(
+ val data: Data,
+ val status: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Attribution.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Attribution.kt
new file mode 100644
index 00000000..34cd9713
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Attribution.kt
@@ -0,0 +1,15 @@
+package hpsaturn.pollutionreporter.dashboard.data.models
+
+
+/**
+ * Attributions of the administrator of the air station.
+ * @property logo Logo of the administrator of the station.
+ * @property name Name of the administrator of the station.
+ * @property name Url of the administrator of the station.
+ * See more here: https://aqicn.org/json-api/doc/
+ */
+data class Attribution(
+ val logo: String,
+ val name: String,
+ val url: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/City.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/City.kt
new file mode 100644
index 00000000..1e1c049a
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/City.kt
@@ -0,0 +1,16 @@
+package hpsaturn.pollutionreporter.dashboard.data.models
+
+
+/**
+ * Information about the monitoring station.
+ * @property name Name of the monitoring station.
+ * @property geo Latitude/Longitude of the monitoring station.
+ * @property url for the attribution link.
+ * See more here: https://aqicn.org/json-api/doc/
+ */
+
+data class City(
+ val geo: List,
+ val name: String,
+ val url: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Data.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Data.kt
new file mode 100644
index 00000000..99f6db6a
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Data.kt
@@ -0,0 +1,20 @@
+package hpsaturn.pollutionreporter.dashboard.data.models
+
+
+/**
+ * Data from the air quality station.
+ * @property aqi Real-time air quality information.
+ * @property attributions List of EPA Attribution for the station.
+ * @property city Information about the monitoring station.
+ * @property idx Unique ID for the city monitoring station.
+ * @property time Measurement time information.
+ * See more here: https://aqicn.org/json-api/doc/
+ */
+
+data class Data(
+ val aqi: Int,
+ val attributions: List,
+ val city: City,
+ val idx: Int,
+ val time: Time
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Time.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Time.kt
new file mode 100644
index 00000000..d801cf17
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/models/Time.kt
@@ -0,0 +1,13 @@
+package hpsaturn.pollutionreporter.dashboard.data.models
+
+
+/**
+ * Measurement time information.
+ * @property s Local measurement time time.
+ * @property tz Station timezone.
+ * See more here: https://aqicn.org/json-api/doc/
+ */
+data class Time(
+ val s: String,
+ val tz: String
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/repositories/AirQualityStatusRepositoryImpl.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/repositories/AirQualityStatusRepositoryImpl.kt
new file mode 100644
index 00000000..683167b3
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/repositories/AirQualityStatusRepositoryImpl.kt
@@ -0,0 +1,40 @@
+package hpsaturn.pollutionreporter.dashboard.data.repositories
+
+import android.content.Context
+import dagger.hilt.android.qualifiers.ApplicationContext
+import hpsaturn.pollutionreporter.R
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.core.domain.errors.ConnectionException
+import hpsaturn.pollutionreporter.core.domain.errors.ServerException
+import hpsaturn.pollutionreporter.core.domain.errors.UnexpectedException
+import hpsaturn.pollutionreporter.dashboard.data.models.AqicnFeedResponse
+import hpsaturn.pollutionreporter.dashboard.data.services.AqicnApiFeedService
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import hpsaturn.pollutionreporter.dashboard.domain.repositories.AirQualityStatusRepository
+import java.io.IOException
+import javax.inject.Inject
+
+class AirQualityStatusRepositoryImpl @Inject constructor(
+ private val aqicnApiFeedService: AqicnApiFeedService,
+ private val mapper: Mapper,
+ @ApplicationContext private val context: Context
+) : AirQualityStatusRepository {
+
+ override suspend fun getNearestAirQualityStatus(
+ latitude: Double,
+ longitude: Double
+ ): AirQualityStatus {
+
+ val response = runCatching {
+ aqicnApiFeedService.getGeolocationFeed(latitude, longitude)
+ }.getOrElse {
+ if (it is IOException) throw ConnectionException(context.getString(R.string.internet_connection_unavailable))
+ else throw UnexpectedException(context.getString(R.string.unexpected_error))
+ }
+ val aqicnFeedResponse = response.body()
+ if (!response.isSuccessful || aqicnFeedResponse == null) {
+ throw ServerException(context.getString(R.string.server_unavailable))
+ }
+ return mapper(aqicnFeedResponse)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/services/AqicnApiFeedService.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/services/AqicnApiFeedService.kt
new file mode 100644
index 00000000..19a7a523
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/data/services/AqicnApiFeedService.kt
@@ -0,0 +1,14 @@
+package hpsaturn.pollutionreporter.dashboard.data.services
+
+import hpsaturn.pollutionreporter.dashboard.data.models.AqicnFeedResponse
+import retrofit2.Response
+import retrofit2.http.GET
+import retrofit2.http.Path
+
+interface AqicnApiFeedService {
+ @GET("feed/geo:{lat};{long}/")
+ suspend fun getGeolocationFeed(
+ @Path("lat") latitude: Double,
+ @Path("long") longitude: Double
+ ): Response
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityScale.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityScale.kt
new file mode 100644
index 00000000..34dc7b80
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityScale.kt
@@ -0,0 +1,15 @@
+package hpsaturn.pollutionreporter.dashboard.domain.entities
+
+import hpsaturn.pollutionreporter.R
+
+enum class AirQualityScale(val colorResourceId: Int, val nameResourceId: Int) {
+ GOOD(R.color.scale_good, R.string.scale_good),
+ MODERATE(R.color.scale_moderate, R.string.scale_moderate),
+ UNHEALTHY_FOR_SENSITIVE_GROUPS(
+ R.color.scale_unhealthy_for_sensitive_groups,
+ R.string.scale_unhealthy_for_sensitive_groups
+ ),
+ UNHEALTHY(R.color.scale_unhealthy, R.string.scale_unhealthy),
+ VERY_UNHEALTHY(R.color.scale_very_unhealthy, R.string.scale_very_unhealthy),
+ HAZARDOUS(R.color.scale_hazardous, R.string.scale_hazardous)
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityStatus.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityStatus.kt
new file mode 100644
index 00000000..94672757
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/entities/AirQualityStatus.kt
@@ -0,0 +1,16 @@
+package hpsaturn.pollutionreporter.dashboard.domain.entities
+
+/**
+ * Air quality information.
+ * @property airQualityIndex Real-time air quality information.
+ * @property stationName Name of the monitoring station.
+ * @property stationLongitude Longitude of the monitoring station.
+ * @property stationLatitude Latitude of the monitoring station.
+ */
+
+class AirQualityStatus(
+ val airQualityIndex: Int,
+ val stationName: String,
+ val stationLatitude: Double,
+ val stationLongitude: Double
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/repositories/AirQualityStatusRepository.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/repositories/AirQualityStatusRepository.kt
new file mode 100644
index 00000000..1fc68962
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/repositories/AirQualityStatusRepository.kt
@@ -0,0 +1,10 @@
+package hpsaturn.pollutionreporter.dashboard.domain.repositories
+
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+
+interface AirQualityStatusRepository {
+ suspend fun getNearestAirQualityStatus(
+ latitude: Double,
+ longitude: Double
+ ): AirQualityStatus
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/EvaluateAirQualityStatus.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/EvaluateAirQualityStatus.kt
new file mode 100644
index 00000000..857087a6
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/EvaluateAirQualityStatus.kt
@@ -0,0 +1,19 @@
+package hpsaturn.pollutionreporter.dashboard.domain.usecases
+
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityScale
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import javax.inject.Inject
+
+
+class EvaluateAirQualityStatus @Inject constructor() {
+ operator fun invoke(airQualityStatus: AirQualityStatus): AirQualityScale =
+ when (airQualityStatus.airQualityIndex) {
+ in Int.MIN_VALUE..-1 -> throw IllegalArgumentException("No negative values for AQI.")
+ in 0..50 -> AirQualityScale.GOOD
+ in 51..100 -> AirQualityScale.MODERATE
+ in 101..150 -> AirQualityScale.UNHEALTHY_FOR_SENSITIVE_GROUPS
+ in 151..200 -> AirQualityScale.UNHEALTHY
+ in 201..300 -> AirQualityScale.VERY_UNHEALTHY
+ else -> AirQualityScale.HAZARDOUS
+ }
+}
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/FindNearestAirQualityStatus.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/FindNearestAirQualityStatus.kt
new file mode 100644
index 00000000..411ce047
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/domain/usecases/FindNearestAirQualityStatus.kt
@@ -0,0 +1,12 @@
+package hpsaturn.pollutionreporter.dashboard.domain.usecases
+
+import hpsaturn.pollutionreporter.dashboard.domain.repositories.AirQualityStatusRepository
+import javax.inject.Inject
+
+class FindNearestAirQualityStatus @Inject constructor(
+ private val airQualityStatusRepository: AirQualityStatusRepository
+) {
+
+ suspend operator fun invoke(latitude: Double, longitude: Double) =
+ airQualityStatusRepository.getNearestAirQualityStatus(latitude, longitude)
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/CurrentLocationLiveData.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/CurrentLocationLiveData.kt
new file mode 100644
index 00000000..9519974c
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/CurrentLocationLiveData.kt
@@ -0,0 +1,108 @@
+package hpsaturn.pollutionreporter.dashboard.presentation
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.pm.PackageManager
+import android.location.Location
+import androidx.core.app.ActivityCompat
+import androidx.lifecycle.LiveData
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationCallback
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationResult
+import com.karumi.dexter.DexterBuilder
+import com.karumi.dexter.MultiplePermissionsReport
+import com.karumi.dexter.PermissionToken
+import com.karumi.dexter.listener.PermissionRequest
+import com.karumi.dexter.listener.multi.MultiplePermissionsListener
+import dagger.hilt.android.qualifiers.ApplicationContext
+import hpsaturn.pollutionreporter.R
+import hpsaturn.pollutionreporter.core.domain.entities.ErrorResult
+import hpsaturn.pollutionreporter.core.domain.entities.InProgress
+import hpsaturn.pollutionreporter.core.domain.entities.Result
+import hpsaturn.pollutionreporter.core.domain.entities.Success
+import hpsaturn.pollutionreporter.core.domain.errors.PermissionException
+import hpsaturn.pollutionreporter.core.domain.errors.PermissionNotGrantedException
+import javax.inject.Inject
+
+class CurrentLocationLiveData @Inject constructor(
+ private val fusedLocationProviderClient: FusedLocationProviderClient,
+ private val locationRequest: LocationRequest,
+ private val dexter: DexterBuilder.Permission,
+ @ApplicationContext private val context: Context
+) : LiveData>() {
+
+ private val setLocationListener = { location: Location -> value = Success(location) }
+
+ @SuppressLint("MissingPermission")
+ override fun onActive() {
+ super.onActive()
+ postValue(InProgress)
+ checkPermissions { startLocationUpdates() }
+ fusedLocationProviderClient.lastLocation.addOnSuccessListener {
+ if (it == null) {
+ postValue(ErrorResult(PermissionNotGrantedException(context.getString(R.string.check_location_settings))))
+ } else {
+ it.also { setLocationListener(it) }
+ }
+ }
+ }
+
+ private fun checkPermissions(onPermissionsGranted: () -> Unit) {
+ dexter.withPermissions(
+ Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ).withListener(object : MultiplePermissionsListener {
+ override fun onPermissionsChecked(multiplePermissionsReport: MultiplePermissionsReport?) {
+ if (multiplePermissionsReport?.areAllPermissionsGranted() == true) {
+ onPermissionsGranted()
+ } else {
+ postValue(ErrorResult(PermissionNotGrantedException(context.getString(R.string.location_permissions_not_granted_error))))
+ }
+ }
+
+ override fun onPermissionRationaleShouldBeShown(
+ requests: MutableList?, token: PermissionToken?
+ ) {
+ postValue(ErrorResult(PermissionException(context.getString(R.string.enable_permission_request))))
+ token?.continuePermissionRequest()
+ }
+
+ }).withErrorListener { postValue(ErrorResult(IllegalAccessException(it.name))) }.check()
+ }
+
+ override fun onInactive() {
+ super.onInactive()
+ fusedLocationProviderClient.removeLocationUpdates(locationCallback)
+ }
+
+ private val locationCallback = object : LocationCallback() {
+ override fun onLocationResult(locationResult: LocationResult?) {
+ locationResult ?: return
+ for (location in locationResult.locations) {
+ setLocationListener(location)
+ }
+ }
+ }
+
+ private fun areLocationPermissionsGranted(): Boolean {
+ return ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_FINE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(
+ context,
+ Manifest.permission.ACCESS_COARSE_LOCATION
+ ) != PackageManager.PERMISSION_GRANTED
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun startLocationUpdates() {
+ if (areLocationPermissionsGranted()) {
+ postValue(ErrorResult(PermissionNotGrantedException(context.getString(R.string.location_permissions_not_granted_error))))
+ return
+ }
+ fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, null)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardFragment.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardFragment.kt
new file mode 100644
index 00000000..e45dcb28
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardFragment.kt
@@ -0,0 +1,90 @@
+package hpsaturn.pollutionreporter.dashboard.presentation
+
+import android.annotation.SuppressLint
+import android.graphics.drawable.GradientDrawable
+import android.graphics.drawable.RotateDrawable
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.content.ContextCompat
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.Observer
+import dagger.hilt.android.AndroidEntryPoint
+import hpsaturn.pollutionreporter.R
+import hpsaturn.pollutionreporter.core.domain.entities.ErrorResult
+import hpsaturn.pollutionreporter.core.domain.entities.InProgress
+import hpsaturn.pollutionreporter.core.domain.entities.Success
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import hpsaturn.pollutionreporter.dashboard.domain.usecases.EvaluateAirQualityStatus
+import kotlinx.android.synthetic.main.fragment_dashboard.*
+import javax.inject.Inject
+
+@AndroidEntryPoint
+class DashboardFragment : Fragment() {
+
+ @Inject
+ lateinit var evaluateAirQualityStatus: EvaluateAirQualityStatus
+
+ private val dashboardViewModel: DashboardViewModel by viewModels()
+
+ override fun onCreateView(
+ inflater: LayoutInflater, container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? = inflater.inflate(R.layout.fragment_dashboard, container, false)
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ displayAirQualityIndexOnView()
+ displayStationDistance()
+ }
+
+ private fun displayAirQualityIndexOnView() {
+ dashboardViewModel.airQualityStatus.observe(viewLifecycleOwner, Observer {
+ when (it) {
+ is Success -> renderAqiData(it.data)
+ is ErrorResult -> renderError(it.exception)
+ is InProgress -> renderProgress()
+ }
+ })
+ }
+
+ @SuppressLint("SetTextI18n")
+ private fun displayStationDistance() {
+ dashboardViewModel.distanceToStation.observe(viewLifecycleOwner, Observer {
+ currentLocationText.text = "$it Km"
+ })
+ }
+
+ private fun renderAqiData(airQualityStatus: AirQualityStatus) {
+ setTextVisible()
+ val scale = evaluateAirQualityStatus(airQualityStatus)
+ val background =
+ (airQualityIndexBar.progressDrawable as RotateDrawable).drawable as GradientDrawable
+ val color = ContextCompat.getColor(requireContext(), scale.colorResourceId)
+ background.colors =
+ intArrayOf(color, color) // Both the same because we don't have a gradient.
+ airQualityIndexText.text = "${airQualityStatus.airQualityIndex}"
+ airQualityLabelText.text = getString(scale.nameResourceId)
+ }
+
+ private fun renderError(exception: Throwable) {
+ setTextVisible()
+ airQualityIndexText.text = context?.getString(R.string.error)
+ airQualityLabelText.text = "${exception.message}"
+ }
+
+ private fun renderProgress() {
+ progressBar.visibility = View.VISIBLE
+ airQualityIndexText.visibility = View.INVISIBLE
+ airQualityLabelText.visibility = View.INVISIBLE
+ }
+
+ private fun setTextVisible() {
+ progressBar.visibility = View.INVISIBLE
+ airQualityIndexText.visibility = View.VISIBLE
+ airQualityLabelText.visibility = View.VISIBLE
+ }
+
+}
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardViewModel.kt b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardViewModel.kt
new file mode 100644
index 00000000..a69f15b1
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/dashboard/presentation/DashboardViewModel.kt
@@ -0,0 +1,64 @@
+package hpsaturn.pollutionreporter.dashboard.presentation
+
+import android.location.Location
+import androidx.hilt.lifecycle.ViewModelInject
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.liveData
+import androidx.lifecycle.switchMap
+import hpsaturn.pollutionreporter.core.domain.entities.ErrorResult
+import hpsaturn.pollutionreporter.core.domain.entities.InProgress
+import hpsaturn.pollutionreporter.core.domain.entities.Result
+import hpsaturn.pollutionreporter.core.domain.entities.Success
+import hpsaturn.pollutionreporter.dashboard.domain.entities.AirQualityStatus
+import hpsaturn.pollutionreporter.dashboard.domain.usecases.FindNearestAirQualityStatus
+import hpsaturn.pollutionreporter.di.DispatchersModule
+import hpsaturn.pollutionreporter.util.combineWith
+import hpsaturn.pollutionreporter.util.round
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+class DashboardViewModel @ViewModelInject constructor(
+ private val findNearestAirQualityStatus: FindNearestAirQualityStatus,
+ currentLocationLiveData: LiveData>,
+ @DispatchersModule.IoDispatcher private val ioDispatcher: CoroutineDispatcher
+) : ViewModel() {
+
+ val airQualityStatus: LiveData> = currentLocationLiveData.switchMap {
+ liveData {
+ when (it) {
+ is Success -> emit(resolveResult(it.data))
+ is ErrorResult -> emit(ErrorResult(it.exception))
+ is InProgress -> emit(InProgress)
+ }
+ }
+ }
+
+ private val numberOfDecimals = 2
+ private val metersInOneKilometer = 1000.0
+
+ private val calculateDistanceInKm =
+ { location: Result?, aqi: Result? ->
+ val aqiLocation = Location("")
+ if (location is Success && aqi is Success) {
+ aqiLocation.longitude = aqi.data.stationLongitude
+ aqiLocation.latitude = aqi.data.stationLatitude
+ val distance = location.data.distanceTo(aqiLocation).toDouble() // Result in meters.
+ (distance / metersInOneKilometer).round(numberOfDecimals)
+ } else {
+ 0.0
+ }
+ }
+
+ val distanceToStation: LiveData =
+ currentLocationLiveData.combineWith(airQualityStatus, calculateDistanceInKm)
+
+ private suspend fun resolveResult(location: Location): Result =
+ withContext(ioDispatcher) {
+ runCatching {
+ Success(findNearestAirQualityStatus(location.latitude, location.longitude))
+ }.getOrElse { e -> ErrorResult(e) }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/di/ApplicationModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/di/ApplicationModule.kt
new file mode 100644
index 00000000..54bc6efa
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/di/ApplicationModule.kt
@@ -0,0 +1,32 @@
+package hpsaturn.pollutionreporter.di
+
+import android.content.Context
+import com.google.firebase.database.DatabaseReference
+import com.google.firebase.database.ktx.database
+import com.google.firebase.ktx.Firebase
+import com.karumi.dexter.Dexter
+import com.karumi.dexter.DexterBuilder.Permission
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.util.*
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ApplicationComponent::class)
+object ApplicationModule {
+ @Singleton
+ @Provides
+ fun provideDexter(@ApplicationContext context: Context): Permission =
+ Dexter.withContext(context)
+
+ @Singleton
+ @Provides
+ fun provideCalendar(): Calendar = Calendar.getInstance()
+
+ @Singleton
+ @Provides
+ fun provideDatabaseReference(): DatabaseReference = Firebase.database.reference
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/di/DispatchersModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/di/DispatchersModule.kt
new file mode 100644
index 00000000..9d8659c1
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/di/DispatchersModule.kt
@@ -0,0 +1,44 @@
+package hpsaturn.pollutionreporter.di
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ApplicationComponent::class)
+object DispatchersModule {
+
+ @Retention(AnnotationRetention.BINARY)
+ @Qualifier
+ annotation class DefaultDispatcher
+
+ @Retention(AnnotationRetention.BINARY)
+ @Qualifier
+ annotation class IoDispatcher
+
+ @Retention(AnnotationRetention.BINARY)
+ @Qualifier
+ annotation class MainDispatcher
+
+ @Singleton
+ @IoDispatcher
+ @Provides
+ fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+ @Singleton
+ @DefaultDispatcher
+ @Provides
+ fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
+
+
+ @Singleton
+ @MainDispatcher
+ @Provides
+ fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/di/LocationModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/di/LocationModule.kt
new file mode 100644
index 00000000..e2b14adf
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/di/LocationModule.kt
@@ -0,0 +1,30 @@
+package hpsaturn.pollutionreporter.di
+
+import android.content.Context
+import com.google.android.gms.location.FusedLocationProviderClient
+import com.google.android.gms.location.LocationRequest
+import com.google.android.gms.location.LocationServices
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import java.util.concurrent.TimeUnit
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ApplicationComponent::class)
+object LocationModule {
+ @Singleton
+ @Provides
+ fun provideFusedLocationProviderClient(@ApplicationContext context: Context):
+ FusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(context)
+
+ @Singleton
+ @Provides
+ fun provideFusedLocationRequest(): LocationRequest = LocationRequest.create().apply {
+ interval = TimeUnit.SECONDS.toMillis(10)
+ fastestInterval = TimeUnit.SECONDS.toMillis(5)
+ priority = LocationRequest.PRIORITY_HIGH_ACCURACY
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/di/NetworkModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/di/NetworkModule.kt
new file mode 100644
index 00000000..448d72d1
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/di/NetworkModule.kt
@@ -0,0 +1,75 @@
+package hpsaturn.pollutionreporter.di
+
+import android.content.Context
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import hpsaturn.pollutionreporter.R
+import okhttp3.Interceptor
+import okhttp3.OkHttpClient
+import okhttp3.logging.HttpLoggingInterceptor
+import retrofit2.Retrofit
+import retrofit2.converter.gson.GsonConverterFactory
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Module
+@InstallIn(ApplicationComponent::class)
+object NetworkModule {
+
+ @Qualifier
+ @Retention(AnnotationRetention.BINARY)
+ annotation class AuthInterceptorOkHttpClient
+
+ @Singleton
+ @AuthInterceptorOkHttpClient
+ @Provides
+ fun provideAuthInterceptorOkHttpClient(@ApplicationContext context: Context) =
+ Interceptor { chain ->
+ val tokenQueryName = context.getString(R.string.api_aqicn_token_query_name)
+ val token = context.getString(R.string.api_aqicn_key)
+ val url = chain.request()
+ .url
+ .newBuilder()
+ .addQueryParameter(tokenQueryName, token)
+ .build()
+ val request = chain.request()
+ .newBuilder()
+ .url(url)
+ .build()
+ return@Interceptor chain.proceed(request)
+ }
+
+ @Singleton
+ @Provides
+ fun provideHttpLoggingInterceptor() = HttpLoggingInterceptor().apply {
+ level = HttpLoggingInterceptor.Level.BODY
+ }
+
+
+ @Singleton
+ @Provides
+ fun provideOkHttpClient(
+ @AuthInterceptorOkHttpClient tokenInterceptor: Interceptor,
+ loggingInterceptor: HttpLoggingInterceptor
+ ) =
+ OkHttpClient.Builder()
+ .addInterceptor(tokenInterceptor)
+ .addInterceptor(loggingInterceptor)
+ .build()
+
+ @Singleton
+ @Provides
+ fun provideRetrofitInstance(
+ @ApplicationContext context: Context,
+ okHttpClient: OkHttpClient
+ ): Retrofit =
+ Retrofit.Builder()
+ .baseUrl(context.getString(R.string.api_aqicn_url))
+ .client(okHttpClient)
+ .addConverterFactory(GsonConverterFactory.create())
+ .build()
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/SensorReportModule.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/SensorReportModule.kt
new file mode 100644
index 00000000..3cd294bd
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/SensorReportModule.kt
@@ -0,0 +1,42 @@
+package hpsaturn.pollutionreporter.reports
+
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ApplicationComponent
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.reports.open.data.mappers.SensorDataPointMapper
+import hpsaturn.pollutionreporter.reports.open.data.mappers.SensorReportInformationMapper
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksData
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksInfo
+import hpsaturn.pollutionreporter.reports.open.data.repositories.OpenSensorReportsRepositoryImpl
+import hpsaturn.pollutionreporter.reports.open.data.services.PublicSensorReportService
+import hpsaturn.pollutionreporter.reports.open.data.services.PublicSensorReportServiceImp
+import hpsaturn.pollutionreporter.reports.open.domain.entities.SensorDataPoint
+import hpsaturn.pollutionreporter.reports.open.domain.entities.SensorReportInformation
+import hpsaturn.pollutionreporter.reports.open.domain.repositories.OpenSensorReportsRepository
+
+@Module
+@InstallIn(ApplicationComponent::class)
+abstract class SensorReportModule {
+
+ @Binds
+ abstract fun bindSensorDataPointMapper(
+ sensorDataPointMapper: SensorDataPointMapper
+ ): Mapper
+
+ @Binds
+ abstract fun bindSensorReportInformationMapper(
+ sensorDataPointMapper: SensorReportInformationMapper
+ ): Mapper
+
+ @Binds
+ abstract fun bindSensorReportRepositoryImpl(
+ sensorReportRepositoryImpl: OpenSensorReportsRepositoryImpl
+ ): OpenSensorReportsRepository
+
+ @Binds
+ abstract fun bindPublicSensorReportServiceImpl(
+ publicSensorReportServiceImp: PublicSensorReportServiceImp
+ ): PublicSensorReportService
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorDataPointMapper.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorDataPointMapper.kt
new file mode 100644
index 00000000..49a3fdfe
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorDataPointMapper.kt
@@ -0,0 +1,19 @@
+package hpsaturn.pollutionreporter.reports.open.data.mappers
+
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksData
+import hpsaturn.pollutionreporter.reports.open.domain.entities.SensorDataPoint
+import hpsaturn.pollutionreporter.util.toUnixTimeStamp
+import javax.inject.Inject
+
+class SensorDataPointMapper @Inject constructor() : Mapper {
+ override fun invoke(input: TracksData): SensorDataPoint = SensorDataPoint(
+ input.id,
+ input.p10,
+ input.p25,
+ input.spd,
+ input.latitude,
+ input.longitude,
+ input.timestamp.toUnixTimeStamp()
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorReportInformationMapper.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorReportInformationMapper.kt
new file mode 100644
index 00000000..64759118
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/mappers/SensorReportInformationMapper.kt
@@ -0,0 +1,18 @@
+package hpsaturn.pollutionreporter.reports.open.data.mappers
+
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksInfo
+import hpsaturn.pollutionreporter.reports.open.domain.entities.SensorReportInformation
+import javax.inject.Inject
+
+class SensorReportInformationMapper @Inject constructor() :
+ Mapper {
+ override fun invoke(input: TracksInfo): SensorReportInformation = SensorReportInformation(
+ input.deviceId,
+ input.date,
+ input.lastLat,
+ input.lastLon,
+ input.name,
+ input.size
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksData.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksData.kt
new file mode 100644
index 00000000..3ce2b86f
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksData.kt
@@ -0,0 +1,23 @@
+package hpsaturn.pollutionreporter.reports.open.data.models
+
+/**
+ * Information of single data point of a report.
+ * @property altitude altitude of sea level of the data point.
+ * @property p10 value of the P10 contaminant.
+ * @property p25 value of the P2.5 contaminant.
+ * @property spd value of SPD.
+ * @property latitude Latitude of the data point.
+ * @property longitude Longitude of the data point.
+ * @property timestamp Timestamp of the data point.
+ */
+
+class TracksData(
+ val id: String,
+ val altitude: Double,
+ val p10: Double,
+ val p25: Double,
+ val spd: Double,
+ val latitude: Double,
+ val longitude: Double,
+ val timestamp: Long
+)
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksInfo.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksInfo.kt
new file mode 100644
index 00000000..402134a7
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/models/TracksInfo.kt
@@ -0,0 +1,21 @@
+package hpsaturn.pollutionreporter.reports.open.data.models
+
+/**
+ * Basic information of a sensor report.
+ * @property deviceId ID of the device that gathered the data.
+ * @property date Date of the report.
+ * @property lastLat Latitude of the last data point.
+ * @property lastLon Longitude of the last data point.
+ * @property name Name of the device that gathered the data.
+ * @property size Number of data points gathered.
+ */
+
+data class TracksInfo(
+ val date: String = "",
+ val deviceId: String = "",
+ val lastLat: Double = 0.0,
+ val lastLon: Double = 0.0,
+ val lastTrackData: TracksData? = null,
+ val name: String = "",
+ val size: Int = 0
+)
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/repositories/OpenSensorReportsRepositoryImpl.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/repositories/OpenSensorReportsRepositoryImpl.kt
new file mode 100644
index 00000000..c2c43f35
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/repositories/OpenSensorReportsRepositoryImpl.kt
@@ -0,0 +1,24 @@
+package hpsaturn.pollutionreporter.reports.open.data.repositories
+
+import hpsaturn.pollutionreporter.core.data.mappers.Mapper
+import hpsaturn.pollutionreporter.core.domain.entities.ErrorResult
+import hpsaturn.pollutionreporter.core.domain.entities.Result
+import hpsaturn.pollutionreporter.core.domain.entities.Success
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksInfo
+import hpsaturn.pollutionreporter.reports.open.data.services.PublicSensorReportService
+import hpsaturn.pollutionreporter.reports.open.domain.entities.SensorReportInformation
+import hpsaturn.pollutionreporter.reports.open.domain.repositories.OpenSensorReportsRepository
+import javax.inject.Inject
+
+class OpenSensorReportsRepositoryImpl @Inject constructor(
+ private val sensorReportService: PublicSensorReportService,
+ private val mapper: Mapper
+) : OpenSensorReportsRepository {
+ override suspend fun getPublicSensorReports(): Result> =
+ runCatching {
+ Success(sensorReportService.getTracksInfo().map { mapper(it) })
+ }.getOrElse {
+ ErrorResult(it)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/services/OpenSensorReportsService.kt b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/services/OpenSensorReportsService.kt
new file mode 100644
index 00000000..9bdcb871
--- /dev/null
+++ b/app/src/main/java/hpsaturn/pollutionreporter/reports/open/data/services/OpenSensorReportsService.kt
@@ -0,0 +1,33 @@
+package hpsaturn.pollutionreporter.reports.open.data.services
+
+import com.google.firebase.database.DatabaseReference
+import com.google.firebase.database.GenericTypeIndicator
+import hpsaturn.pollutionreporter.reports.open.data.models.TracksInfo
+import hpsaturn.pollutionreporter.reports.open.domain.entities.TracksInfoNotFoundException
+import hpsaturn.pollutionreporter.util.getSuspendValue
+import javax.inject.Inject
+
+interface PublicSensorReportService {
+ suspend fun getTracksInfo(): List
+}
+
+class PublicSensorReportServiceImp @Inject constructor(
+ private val database: DatabaseReference
+) : PublicSensorReportService {
+
+ override suspend fun getTracksInfo(): List {
+ val result = database
+ .child(TRACKS_INFO_COLLECTION)
+ .limitToLast(20)
+ .getSuspendValue()
+ .getValue(object :
+ GenericTypeIndicator