diff --git a/Project/.idea/compiler.xml b/Project/.idea/compiler.xml
new file mode 100644
index 0000000..61a9130
--- /dev/null
+++ b/Project/.idea/compiler.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Project/.idea/misc.xml b/Project/.idea/misc.xml
index f5c6d9e..1493222 100644
--- a/Project/.idea/misc.xml
+++ b/Project/.idea/misc.xml
@@ -1,4 +1,4 @@
-
+
\ No newline at end of file
diff --git a/Project/app/src/main/java/org/ionproject/android/common/IonApplication.kt b/Project/app/src/main/java/org/ionproject/android/common/IonApplication.kt
index 4dbbfe5..9630d1b 100644
--- a/Project/app/src/main/java/org/ionproject/android/common/IonApplication.kt
+++ b/Project/app/src/main/java/org/ionproject/android/common/IonApplication.kt
@@ -1,6 +1,8 @@
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
@@ -8,6 +10,7 @@ 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
@@ -37,6 +40,7 @@ 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() {
@@ -44,6 +48,8 @@ class IonApplication : Application() {
globalExceptionHandler = GlobalExceptionHandler()
+ preferences = Preferences(applicationContext)
+
/**
* Our app runs in a single process therefore we follow
* the singleton design pattern when instantiating an
@@ -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()
@@ -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)
+ }
}
diff --git a/Project/app/src/main/java/org/ionproject/android/common/ionwebapi/IonService.kt b/Project/app/src/main/java/org/ionproject/android/common/ionwebapi/IonService.kt
index 9e313bb..e9189ff 100644
--- a/Project/app/src/main/java/org/ionproject/android/common/ionwebapi/IonService.kt
+++ b/Project/app/src/main/java/org/ionproject/android/common/ionwebapi/IonService.kt
@@ -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
-
}
diff --git a/Project/app/src/main/java/org/ionproject/android/common/repositories/RootRepository.kt b/Project/app/src/main/java/org/ionproject/android/common/repositories/RootRepository.kt
index 276bd73..ebc2e4f 100644
--- a/Project/app/src/main/java/org/ionproject/android/common/repositories/RootRepository.kt
+++ b/Project/app/src/main/java/org/ionproject/android/common/repositories/RootRepository.kt
@@ -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.
@@ -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
@@ -50,4 +47,5 @@ class RootRepository(
}
rootResource
}
+
}
\ No newline at end of file
diff --git a/Project/app/src/main/java/org/ionproject/android/error/ErrorActivity.kt b/Project/app/src/main/java/org/ionproject/android/error/ErrorActivity.kt
index 474a8f6..75499d2 100644
--- a/Project/app/src/main/java/org/ionproject/android/error/ErrorActivity.kt
+++ b/Project/app/src/main/java/org/ionproject/android/error/ErrorActivity.kt
@@ -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.*
@@ -24,6 +25,5 @@ class ErrorActivity : AppCompatActivity() {
intent.getStringExtra(ERROR_ACTIVITY_EXCEPTION_EXTRA)?.apply {
textview_error_activity_message.text = this
}
-
}
}
diff --git a/Project/app/src/main/java/org/ionproject/android/loading/LoadingActivity.kt b/Project/app/src/main/java/org/ionproject/android/loading/LoadingActivity.kt
index 0808782..7865230 100644
--- a/Project/app/src/main/java/org/ionproject/android/loading/LoadingActivity.kt
+++ b/Project/app/src/main/java/org/ionproject/android/loading/LoadingActivity.kt
@@ -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) {
@@ -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 -> {
+ val intent = Intent(this, MainActivity::class.java)
+ intent.putExtra(MAIN_ACTIVITY_ROOT_EXTRA, it.value)
+ this.startActivity(intent)
+ }
+ is FetchFailure -> {
+ 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 -> loadingViewModel.getJsonHome(URI(it.value.api_link))
+ is FetchFailure -> {
+ 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)))
}
}
diff --git a/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModel.kt b/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModel.kt
index 45030cf..815e423 100644
--- a/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModel.kt
+++ b/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModel.kt
@@ -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()
+/**
+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
+data class FetchFailure(val throwable: Throwable? = null) : FetchResult()
+data class FetchSuccess(val value: T) : FetchResult()
+
+class LoadingViewModel(
+ private val rootRepository: RootRepository,
+ private val remoteConfigRepository: RemoteConfigRepository
+) : ViewModel() {
+
+ private val rootLiveData = MutableLiveData>()
+ private val remoteConfigLiveData = MutableLiveData>()
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()
+ } catch (e: Exception) {
+ FetchFailure(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()
+ } catch (e: Exception) {
+ FetchFailure(e)
+ }
+
+ remoteConfigLiveData.postValue(result)
+ }
+ }
+
+ fun observeRootLiveData(lifecycleOwner: LifecycleOwner, onUpdate: (FetchResult) -> Unit) {
rootLiveData.observe(lifecycleOwner, Observer { onUpdate(it) })
}
+
+ fun observeRemoteConfigLiveData(
+ lifecycleOwner: LifecycleOwner,
+ onUpdate: (FetchResult) -> Unit
+ ) {
+ remoteConfigLiveData.observe(lifecycleOwner, Observer { onUpdate(it) })
+ }
+
+ fun getRemoteConfigLiveData() = remoteConfigLiveData.value
}
\ No newline at end of file
diff --git a/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModelProvider.kt b/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModelProvider.kt
index 5c02ffd..070920d 100644
--- a/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModelProvider.kt
+++ b/Project/app/src/main/java/org/ionproject/android/loading/LoadingViewModelProvider.kt
@@ -9,7 +9,7 @@ class LoadingViewModelProvider : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun create(modelClass: Class): 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
}
diff --git a/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfig.kt b/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfig.kt
new file mode 100644
index 0000000..9e411d2
--- /dev/null
+++ b/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfig.kt
@@ -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
+)
\ No newline at end of file
diff --git a/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfigRepository.kt b/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfigRepository.kt
new file mode 100644
index 0000000..f72b07d
--- /dev/null
+++ b/Project/app/src/main/java/org/ionproject/android/loading/RemoteConfigRepository.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/Project/app/src/main/java/org/ionproject/android/settings/Preferences.kt b/Project/app/src/main/java/org/ionproject/android/settings/Preferences.kt
index 220c50c..5232504 100644
--- a/Project/app/src/main/java/org/ionproject/android/settings/Preferences.kt
+++ b/Project/app/src/main/java/org/ionproject/android/settings/Preferences.kt
@@ -2,9 +2,11 @@ package org.ionproject.android.settings
import android.content.Context
import android.content.SharedPreferences
+import org.ionproject.android.common.ionwebapi.WEB_API_HOST
private const val SHARED_PREFERENCES_FILE = "org.ionproject.android.SHARED_PREFERENCES_FILE"
private const val CALENDAR_TERM_KEY = "calendar_term_key"
+private const val WEB_API_HOST_KEY = "webApi_host_key"
/**
* Preferences class, should contain only preferences related operations.
@@ -27,4 +29,11 @@ class Preferences(context: Context) {
putString(CALENDAR_TERM_KEY, calendarTerm)
commit()
}
+
+ fun getWebApiHost() = sharedPref.getString(WEB_API_HOST_KEY, WEB_API_HOST)
+
+ fun saveWebApiHost(newUrl: String) = with(sharedPref.edit()) {
+ putString(WEB_API_HOST_KEY, newUrl)
+ commit()
+ }
}
\ No newline at end of file
diff --git a/Project/app/src/main/res/layout/activity_error.xml b/Project/app/src/main/res/layout/activity_error.xml
index 8fd8040..86515f3 100644
--- a/Project/app/src/main/res/layout/activity_error.xml
+++ b/Project/app/src/main/res/layout/activity_error.xml
@@ -56,7 +56,6 @@
android:layout_marginTop="@dimen/fragments_padding_20"
android:backgroundTint="@color/secondaryLightColor"
android:text="@string/label_button_close_error" />
-
diff --git a/Project/app/src/main/res/values-pt/strings.xml b/Project/app/src/main/res/values-pt/strings.xml
index 28be4c9..31e1bb8 100644
--- a/Project/app/src/main/res/values-pt/strings.xml
+++ b/Project/app/src/main/res/values-pt/strings.xml
@@ -85,6 +85,7 @@
Pedimos desculpa mas ocorreu um erro. Tente reiniciar a aplicação.
Este recurso não está disponível :(
fechar
+ Atualizar e tentar de novo
Nome não disponível
A aplicação i-on Android não guarda dados pessoais do utilizador, guarda apenas informação obtida através da i-on Core API. Além disto, toda a informação guardada é local ao dispositivo e é removida assim que o utilizador desinstala a aplicação.
Dados pessoais
@@ -96,4 +97,6 @@
Exame
Tarefa
Notícia
+ A API i-on não está disponível, a redirecionar para o site do ISEL
+ Ficheiro de configuração remota não disponível, a redirecionar para o site do ISEL
diff --git a/Project/app/src/main/res/values/strings.xml b/Project/app/src/main/res/values/strings.xml
index 47e7dfe..091e37b 100644
--- a/Project/app/src/main/res/values/strings.xml
+++ b/Project/app/src/main/res/values/strings.xml
@@ -55,6 +55,7 @@
export
close
+ Update info and try again
Application seems to be outdated :(
Loading i-on
Name not available
@@ -124,4 +125,6 @@
The i-on Android application does not save any personal user data, it only saves information obtained from the i-on Core API. All the information that is saved stays only local to the device and once the application is uninstalled the information is deleted.
i-on Android is a mobile application developed by members of the i-on iniciative. The iniciative was created with the goal of improving some academic aspects in the Polytechnic Institute of Engineering of Lisbon. The iniciative was created by teachers and students. All the code from this application and the rest of the projects from the iniciative is public so feel free to check it out by clicking the i-on logo below.
+ The i-on API is down, redirecting to ISEL\'s homepage
+ Couldn\'t update from Remote Config, redirecting to ISEL\'s homepage