Skip to content

Commit

Permalink
Merge branch 'master' into enhancement/#93-intent-keys-and-dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
Jtoliveira authored May 5, 2021
2 parents c7213dc + 971ffd4 commit f0dcd26
Show file tree
Hide file tree
Showing 15 changed files with 228 additions and 32 deletions.
6 changes: 6 additions & 0 deletions Project/.idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Project/.idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package org.ionproject.android.common

import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import androidx.room.Room
import org.ionproject.android.common.connectivity.ConnectivityObservableFactory
import org.ionproject.android.common.connectivity.IConnectivityObservable
import org.ionproject.android.common.db.AppDatabase
import org.ionproject.android.common.ionwebapi.*
import org.ionproject.android.common.repositories.*
import org.ionproject.android.common.workers.WorkerManagerFacade
import org.ionproject.android.loading.RemoteConfigRepository
import org.ionproject.android.settings.Preferences
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
Expand Down Expand Up @@ -37,13 +40,16 @@ class IonApplication : Application() {
lateinit var globalExceptionHandler: GlobalExceptionHandler private set
lateinit var preferences: Preferences private set
lateinit var connectivityObservable: IConnectivityObservable private set
lateinit var remoteConfigRepository: RemoteConfigRepository private set
}

override fun onCreate() {
super.onCreate()

globalExceptionHandler = GlobalExceptionHandler()

preferences = Preferences(applicationContext)

/**
* Our app runs in a single process therefore we follow
* the singleton design pattern when instantiating an
Expand All @@ -62,7 +68,7 @@ class IonApplication : Application() {

//------- Using real API -------------
val retrofit = Retrofit.Builder()
.baseUrl(WEB_API_HOST)
.baseUrl(preferences.getWebApiHost()?: WEB_API_HOST)
.addConverterFactory(ScalarsConverterFactory.create())
.build()

Expand Down Expand Up @@ -103,9 +109,8 @@ class IonApplication : Application() {
EventsRepository(db.eventsDao(), webAPI, workerManagerFacade)
rootRepository = RootRepository(db.rootDao(), ionWebAPI, workerManagerFacade)
searchRepository = SearchRepository(webAPI)
preferences =
Preferences(applicationContext)
connectivityObservable = ConnectivityObservableFactory.create(applicationContext)
}

remoteConfigRepository = RemoteConfigRepository(preferences, ionMapper)
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
package org.ionproject.android.common.ionwebapi

import retrofit2.http.GET
import retrofit2.http.Header
import retrofit2.http.Headers
import retrofit2.http.Url
import retrofit2.http.*

interface IonService {

@Headers("Authorization: Bearer $WEB_API_AUTHORIZATION_TOKEN")
@GET
suspend fun getFromUri(
@Url uri: String,
@Header("Accept") accept: String
): String

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ import org.ionproject.android.common.workers.WorkerManagerFacade
import java.net.URI


// This uri has to be hardcoded there is no other way
private val ROOT_URI_V0 = URI("/")

/**
* Used to check the existence of i-on core Web API root endpoints. It does so by
* checking the Root resource.
Expand All @@ -26,14 +23,14 @@ class RootRepository(
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {

suspend fun getJsonHome() =
suspend fun getJsonHome(uri: URI) =
withContext(dispatcher) {
// Verify if root resource is stored locally
var rootResource = rootResourceDao.getRootResource()

if (rootResource == null) {
rootResource =
ionWebAPI.getFromURI(ROOT_URI_V0, JsonHome::class.java, JSON_HOME_MEDIA_TYPE)
ionWebAPI.getFromURI(uri, JsonHome::class.java, JSON_HOME_MEDIA_TYPE)
.toRoot()
if (rootResource != null) {
// Save root resource into local database and create a worker for it
Expand All @@ -50,4 +47,5 @@ class RootRepository(
}
rootResource
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.ionproject.android.error


import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_error.*
Expand All @@ -24,6 +25,5 @@ class ErrorActivity : AppCompatActivity() {
intent.getStringExtra(ERROR_ACTIVITY_EXCEPTION_EXTRA)?.apply {
textview_error_activity_message.text = this
}

}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
package org.ionproject.android.loading

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import kotlinx.android.synthetic.main.activity_loading.*
import org.ionproject.android.ExceptionHandlingActivity
import org.ionproject.android.R
import org.ionproject.android.common.IonApplication
import org.ionproject.android.common.addGradientBackground
import org.ionproject.android.common.model.Root
import org.ionproject.android.error.ERROR_KEY
import org.ionproject.android.error.ErrorActivity
import org.ionproject.android.main.MAIN_ACTIVITY_ROOT_EXTRA
import org.ionproject.android.main.MainActivity
import java.net.URI

/**
* 1) Try and get data from the JsonHome URL
* 2) if it fails, get the URL from the Remote Config file
* 3) it it fails to get that info, check connectivity: open isel's page if it exists, redirect to no connect
* error if it doesn't
*/
class LoadingActivity : ExceptionHandlingActivity() {

private val loadingViewModel by lazy(LazyThreadSafetyMode.NONE) {
Expand All @@ -22,16 +34,66 @@ class LoadingActivity : ExceptionHandlingActivity() {
setContentView(R.layout.activity_loading)
linearlayout_activity_loading.addGradientBackground()

// If JsonHome contains all required resources then open main activity else app must be outdated
/**
* If JsonHome contains all required resources then open main activity; if not app must be outdated
*/
loadingViewModel.observeRootLiveData(this) {
if (it != null) {
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(MAIN_ACTIVITY_ROOT_EXTRA, it)
this.startActivity(intent)
} else {
val intent = Intent(this, ErrorActivity::class.java)
this.startActivity(intent)
when (it) {
is FetchSuccess<Root> -> {
val intent = Intent(this, MainActivity::class.java)
intent.putExtra(MAIN_ACTIVITY_ROOT_EXTRA, it.value)
this.startActivity(intent)
}
is FetchFailure<Root> -> {
if (loadingViewModel.getRemoteConfigLiveData() == null) {
loadingViewModel.getRemoteConfig()
} else {
Toast.makeText(
this,
resources.getString(R.string.ion_api_down),
Toast.LENGTH_LONG
).show()
openISELPage()
}
}
}
}

/**
* We check the connectivity in this observer because, although unlikely, GitHub might
* be down and there needs to be a plan for that
*/
loadingViewModel.observeRemoteConfigLiveData(this) {
when (it) {
is FetchSuccess<RemoteConfig> -> loadingViewModel.getJsonHome(URI(it.value.api_link))
is FetchFailure<RemoteConfig> -> {
if (!IonApplication.connectivityObservable.hasConnectivity()) {
startActivity(
Intent(this, ErrorActivity::class.java)
.putExtra(
ERROR_KEY,
resources.getString(R.string.label_no_connectivity_loading_error)
)
)
} else {
Toast.makeText(
this,
resources.getString(R.string.remote_config_unreachable),
Toast.LENGTH_LONG
).show()
openISELPage()
}
}
}
}
}

/**
* if the remote config link is the same as the link in the app,
it means the API is down and for now we redirect to the ISEL website
*/
private fun openISELPage() {
val iselURL = "https://www.isel.pt"
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(iselURL)))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,79 @@ import androidx.lifecycle.*
import kotlinx.coroutines.launch
import org.ionproject.android.common.model.Root
import org.ionproject.android.common.repositories.RootRepository
import java.net.URI

class LoadingViewModel(private val rootRepository: RootRepository) : ViewModel() {
// This uri has to be hardcoded there is no other way
private val ROOT_URI_V0 = URI("/")

private val rootLiveData = MutableLiveData<Root?>()
/**
getJsonHome() returns null when the API is unavailable. This makes the default null status of
rootLiveData ambiguous, so this class envelops the results of the getJsonHome() method in order to
properly evaluate the state of the API and to launch the Remote Config strat
Fetch Failure with null value: request was made to API and there was no response
Fetch Failure with throwable: request threw an Exception, if not caught triggers the global Exception Handler
from ExceptionHandlingActivity()
Fetch Success : valid response
*/
sealed class FetchResult<out T>
data class FetchFailure<T>(val throwable: Throwable? = null) : FetchResult<T>()
data class FetchSuccess<T>(val value: T) : FetchResult<T>()

class LoadingViewModel(
private val rootRepository: RootRepository,
private val remoteConfigRepository: RemoteConfigRepository
) : ViewModel() {

private val rootLiveData = MutableLiveData<FetchResult<Root>>()
private val remoteConfigLiveData = MutableLiveData<FetchResult<RemoteConfig>>()

init {
getJsonHome(ROOT_URI_V0)
}

/**
Retrofit note: since retrofit 2 uses okhttp's HttpUrl, we don't need to change the base url of
the service in endpoints where the method annotation doesn't specify a url. it detects if the
@URL parameter doesn't match with the base url and sorts everything out by itself
*/
fun getJsonHome(uri: URI) {
viewModelScope.launch {
val root = rootRepository.getJsonHome()
rootLiveData.postValue(root)
val result = try {
val root = rootRepository.getJsonHome(uri)
if (root != null) FetchSuccess(root) else FetchFailure<Root>()
} catch (e: Exception) {
FetchFailure<Root>(e)
}

rootLiveData.postValue(result)
}
}

fun observeRootLiveData(lifecycleOwner: LifecycleOwner, onUpdate: (Root?) -> Unit) {
fun getRemoteConfig() {
viewModelScope.launch {
val result = try {
val remoteConfig = remoteConfigRepository.getRemoteConfig()
if (remoteConfig?.api_link != null) FetchSuccess(remoteConfig) else FetchFailure<RemoteConfig>()
} catch (e: Exception) {
FetchFailure<RemoteConfig>(e)
}

remoteConfigLiveData.postValue(result)
}
}

fun observeRootLiveData(lifecycleOwner: LifecycleOwner, onUpdate: (FetchResult<Root>) -> Unit) {
rootLiveData.observe(lifecycleOwner, Observer { onUpdate(it) })
}

fun observeRemoteConfigLiveData(
lifecycleOwner: LifecycleOwner,
onUpdate: (FetchResult<RemoteConfig>) -> Unit
) {
remoteConfigLiveData.observe(lifecycleOwner, Observer { onUpdate(it) })
}

fun getRemoteConfigLiveData() = remoteConfigLiveData.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class LoadingViewModelProvider : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return when (modelClass) {
LoadingViewModel::class.java -> LoadingViewModel(IonApplication.rootRepository)
LoadingViewModel::class.java -> LoadingViewModel(IonApplication.rootRepository, IonApplication.remoteConfigRepository)
else -> throw IllegalArgumentException("Class $modelClass is not valid for this provider")
} as T
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.ionproject.android.loading

import com.fasterxml.jackson.annotation.JsonProperty

data class RemoteConfig(
@JsonProperty("api_link")
var api_link: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.ionproject.android.loading

import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.ionproject.android.common.ionwebapi.*
import org.ionproject.android.settings.Preferences
import retrofit2.Retrofit
import retrofit2.converter.scalars.ScalarsConverterFactory
import java.net.URI

const val REMOTE_CONFIG_LINK =
"https://raw.githubusercontent.com/i-on-project/isel/main/Remote_Config.json"

class RemoteConfigRepository(private val preferences: Preferences, mapper: JacksonIonMapper) {

private val retrofit = Retrofit.Builder()
.baseUrl("https://raw.githubusercontent.com/")
.addConverterFactory(ScalarsConverterFactory.create())
.build()

private val service: IonService = retrofit.create(IonService::class.java)

private val ionWebAPI = IonWebAPI(service, mapper)

suspend fun getRemoteConfig() =

withContext(Dispatchers.IO) {

var remoteConfig: RemoteConfig?

remoteConfig = ionWebAPI.getFromURI(
URI(REMOTE_CONFIG_LINK),
RemoteConfig::class.java,
"application/json"
)

val storedApiUrl = preferences.getWebApiHost()

//update the stored API URL in case it changed
if (remoteConfig.api_link != storedApiUrl) {
preferences.saveWebApiHost(remoteConfig.api_link)
}
Log.d("API", "remoteConfig in repo: $remoteConfig")

remoteConfig
}
}
Loading

0 comments on commit f0dcd26

Please sign in to comment.