From 8cccef8f2c660a0a4dbe0c7b8897ee76d3d8bdab Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 24 May 2021 15:00:19 +0200 Subject: [PATCH 1/2] fix(#37): Checks if the current user session is still valid as dedicated worker, change backoff criteria for periodic synchronization worker --- commons/build.gradle | 2 +- .../java/fr/geonature/commons/data/AppSync.kt | 14 ++++ .../fr/geonature/commons/data/AppSyncTest.kt | 35 ++++++++- commons/version.properties | 4 +- .../java/fr/geonature/sync/MainApplication.kt | 19 +++++ .../geonature/sync/auth/AuthLoginViewModel.kt | 37 +++++---- .../fr/geonature/sync/data/dao/AppSyncDao.kt | 51 ++++++++---- .../fr/geonature/sync/sync/DataSyncManager.kt | 77 ++++++++++++++++--- .../geonature/sync/sync/DataSyncViewModel.kt | 17 +++- .../sync/sync/worker/CheckAuthLoginWorker.kt | 71 +++++++++++++++++ .../sync/sync/worker/DataSyncWorker.kt | 27 ++++++- .../fr/geonature/sync/ui/home/DataSyncView.kt | 10 ++- .../fr/geonature/sync/ui/home/HomeActivity.kt | 12 ++- .../geonature/sync/ui/login/LoginActivity.kt | 69 ++++++++--------- sync/src/main/res/values-fr/strings.xml | 1 + sync/src/main/res/values/strings.xml | 1 + 16 files changed, 356 insertions(+), 91 deletions(-) create mode 100644 sync/src/main/java/fr/geonature/sync/sync/worker/CheckAuthLoginWorker.kt diff --git a/commons/build.gradle b/commons/build.gradle index 466b412a..81d93a23 100644 --- a/commons/build.gradle +++ b/commons/build.gradle @@ -1,7 +1,7 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -version = "0.8.2" +version = "0.8.3" android { compileSdkVersion 29 diff --git a/commons/src/main/java/fr/geonature/commons/data/AppSync.kt b/commons/src/main/java/fr/geonature/commons/data/AppSync.kt index 5dd7e206..73ab9608 100644 --- a/commons/src/main/java/fr/geonature/commons/data/AppSync.kt +++ b/commons/src/main/java/fr/geonature/commons/data/AppSync.kt @@ -16,12 +16,14 @@ import java.util.Date data class AppSync( var packageId: String, var lastSync: Date? = null, + var lastSyncEssential: Date? = null, var inputsToSynchronize: Int = 0 ) : Parcelable { private constructor(source: Parcel) : this( source.readString()!!, source.readSerializable() as Date, + source.readSerializable() as Date, source.readInt() ) @@ -36,6 +38,7 @@ data class AppSync( dest?.also { it.writeString(packageId) it.writeSerializable(lastSync) + it.writeSerializable(lastSyncEssential) it.writeInt(inputsToSynchronize) } } @@ -47,6 +50,7 @@ data class AppSync( const val TABLE_NAME = "app_sync" const val COLUMN_ID = "package_id" const val COLUMN_LAST_SYNC = "last_sync" + const val COLUMN_LAST_SYNC_ESSENTIAL = "last_sync_essential" const val COLUMN_INPUTS_TO_SYNCHRONIZE = "inputs_to_synchronize" /** @@ -62,6 +66,10 @@ data class AppSync( COLUMN_LAST_SYNC, tableAlias ), + column( + COLUMN_LAST_SYNC_ESSENTIAL, + tableAlias + ), column( COLUMN_INPUTS_TO_SYNCHRONIZE, tableAlias @@ -113,6 +121,12 @@ data class AppSync( tableAlias ) ), + cursor.get( + getColumnAlias( + COLUMN_LAST_SYNC_ESSENTIAL, + tableAlias + ) + ), requireNotNull( cursor.get( getColumnAlias( diff --git a/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt b/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt index f6fd75e5..1dd813c4 100644 --- a/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt +++ b/commons/src/test/java/fr/geonature/commons/data/AppSyncTest.kt @@ -12,6 +12,7 @@ import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.robolectric.RobolectricTestRunner import java.time.Instant +import java.time.temporal.ChronoUnit import java.util.Date /** @@ -29,11 +30,27 @@ class AppSyncTest { assertEquals( AppSync( "fr.geonature.sync", + Date.from( + now + .toInstant() + .minus( + 1, + ChronoUnit.HOURS + ) + ), now, 3 ), AppSync( "fr.geonature.sync", + Date.from( + now + .toInstant() + .minus( + 1, + ChronoUnit.HOURS + ) + ), now, 3 ) @@ -51,8 +68,9 @@ class AppSyncTest { } `when`(cursor.getString(0)).thenReturn("fr.geonature.sync") - `when`(cursor.getLong(1)).thenReturn(1477642500000) - `when`(cursor.getInt(2)).thenReturn(3) + `when`(cursor.getLong(1)).thenReturn(1477638900000) + `when`(cursor.getLong(2)).thenReturn(1477642500000) + `when`(cursor.getInt(3)).thenReturn(3) // when getting AppSync instance from Cursor val appSync = AppSync.fromCursor(cursor) @@ -62,6 +80,7 @@ class AppSyncTest { assertEquals( AppSync( "fr.geonature.sync", + Date.from(Instant.parse("2016-10-28T07:15:00Z")), Date.from(Instant.parse("2016-10-28T08:15:00Z")), 3 ), @@ -74,6 +93,14 @@ class AppSyncTest { // given AppSync val appSync = AppSync( "fr.geonature.sync", + Date.from( + Instant + .now() + .minus( + 1, + ChronoUnit.HOURS + ) + ), Date.from(Instant.now()), 3 ) @@ -107,6 +134,10 @@ class AppSyncTest { "${AppSync.TABLE_NAME}.\"${AppSync.COLUMN_LAST_SYNC}\"", "${AppSync.TABLE_NAME}_${AppSync.COLUMN_LAST_SYNC}" ), + Pair( + "${AppSync.TABLE_NAME}.\"${AppSync.COLUMN_LAST_SYNC_ESSENTIAL}\"", + "${AppSync.TABLE_NAME}_${AppSync.COLUMN_LAST_SYNC_ESSENTIAL}" + ), Pair( "${AppSync.TABLE_NAME}.\"${AppSync.COLUMN_INPUTS_TO_SYNCHRONIZE}\"", "${AppSync.TABLE_NAME}_${AppSync.COLUMN_INPUTS_TO_SYNCHRONIZE}" diff --git a/commons/version.properties b/commons/version.properties index e2563159..ab311b02 100644 --- a/commons/version.properties +++ b/commons/version.properties @@ -1,2 +1,2 @@ -#Sun Mar 28 16:42:31 CEST 2021 -VERSION_CODE=2820 +#Sun May 23 16:23:47 CEST 2021 +VERSION_CODE=2855 diff --git a/sync/src/main/java/fr/geonature/sync/MainApplication.kt b/sync/src/main/java/fr/geonature/sync/MainApplication.kt index d413a3e0..0d5120d9 100644 --- a/sync/src/main/java/fr/geonature/sync/MainApplication.kt +++ b/sync/src/main/java/fr/geonature/sync/MainApplication.kt @@ -10,6 +10,7 @@ import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager import fr.geonature.mountpoint.util.MountPointUtils.getExternalStorage import fr.geonature.mountpoint.util.MountPointUtils.getInternalStorage +import fr.geonature.sync.sync.worker.CheckAuthLoginWorker import fr.geonature.sync.sync.worker.CheckInputsToSynchronizeWorker import java.util.concurrent.TimeUnit @@ -36,9 +37,27 @@ class MainApplication : Application() { configureCheckInputsToSynchronizeChannel(notificationManager) configureSynchronizeDataChannel(notificationManager) + checkAuthLogin() checkInputsToSynchronize() } + private fun checkAuthLogin() { + val workManager: WorkManager = WorkManager.getInstance(this) + + val request = PeriodicWorkRequestBuilder( + 1, + TimeUnit.HOURS + ) + .addTag(CheckAuthLoginWorker.CHECK_AUTH_LOGIN_WORKER_TAG) + .build() + + workManager.enqueueUniquePeriodicWork( + CheckAuthLoginWorker.CHECK_AUTH_LOGIN_WORKER, + ExistingPeriodicWorkPolicy.REPLACE, + request + ) + } + private fun checkInputsToSynchronize() { val workManager: WorkManager = WorkManager.getInstance(this) diff --git a/sync/src/main/java/fr/geonature/sync/auth/AuthLoginViewModel.kt b/sync/src/main/java/fr/geonature/sync/auth/AuthLoginViewModel.kt index 77cc0804..3d6aa8cc 100644 --- a/sync/src/main/java/fr/geonature/sync/auth/AuthLoginViewModel.kt +++ b/sync/src/main/java/fr/geonature/sync/auth/AuthLoginViewModel.kt @@ -30,8 +30,7 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio private val authManager: AuthManager = AuthManager.getInstance(application) private val geoNatureAPIClient: GeoNatureAPIClient? = GeoNatureAPIClient.instance(application) - private val connectivityManager = - application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + private val connectivityManager = application.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager private val _loginFormState = MutableLiveData() val loginFormState: LiveData = _loginFormState @@ -52,6 +51,16 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio } } + fun checkAuthLogin(): LiveData { + val authLoginLiveData = MutableLiveData() + + viewModelScope.launch { + authLoginLiveData.postValue(authManager.getAuthLogin()) + } + + return authLoginLiveData + } + fun login( username: String, password: String, @@ -95,13 +104,13 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio return@launch } - authManager.setAuthLogin(authLogin) + authManager + .setAuthLogin(authLogin) .also { _loginResult.value = LoginResult(success = authLogin) } } catch (e: Exception) { - _loginResult.value = - LoginResult(error = if (connectivityManager.allNetworks.isEmpty()) R.string.snackbar_network_lost else R.string.login_failed) + _loginResult.value = LoginResult(error = if (connectivityManager.allNetworks.isEmpty()) R.string.snackbar_network_lost else R.string.login_failed) } } } @@ -111,14 +120,12 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio password: String ) { if (!isUserNameValid(username)) { - _loginFormState.value = - LoginFormState(usernameError = R.string.login_form_username_invalid) + _loginFormState.value = LoginFormState(usernameError = R.string.login_form_username_invalid) return } if (!isPasswordValid(password)) { - _loginFormState.value = - LoginFormState(passwordError = R.string.login_form_password_invalid) + _loginFormState.value = LoginFormState(passwordError = R.string.login_form_password_invalid) return } @@ -153,7 +160,8 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio val type = object : TypeToken() {}.type return Gson().fromJson( - response.errorBody()!! + response + .errorBody()!! .charStream(), type ) @@ -165,10 +173,8 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio * @author [S. Grimault](mailto:sebastien.grimault@gmail.com) */ data class LoginFormState( - @StringRes - val usernameError: Int? = null, - @StringRes - val passwordError: Int? = null, + @StringRes val usernameError: Int? = null, + @StringRes val passwordError: Int? = null, val isValid: Boolean = false ) @@ -179,8 +185,7 @@ class AuthLoginViewModel(application: Application) : AndroidViewModel(applicatio */ data class LoginResult( val success: AuthLogin? = null, - @StringRes - val error: Int? = null + @StringRes val error: Int? = null ) { fun hasError(): Boolean { diff --git a/sync/src/main/java/fr/geonature/sync/data/dao/AppSyncDao.kt b/sync/src/main/java/fr/geonature/sync/data/dao/AppSyncDao.kt index 5197a497..8a2df848 100644 --- a/sync/src/main/java/fr/geonature/sync/data/dao/AppSyncDao.kt +++ b/sync/src/main/java/fr/geonature/sync/data/dao/AppSyncDao.kt @@ -19,17 +19,20 @@ import java.util.Date */ class AppSyncDao(private val context: Context) { - private val sharedPreferences: SharedPreferences = - PreferenceManager.getDefaultSharedPreferences(context) + private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) fun findByPackageId(packageId: String?): Cursor { - val cursor = MatrixCursor(AppSync.defaultProjection().map { it.second }.toTypedArray()) + val cursor = MatrixCursor(AppSync + .defaultProjection() + .map { it.second } + .toTypedArray()) if (packageId.isNullOrBlank()) return cursor val values = arrayOf( packageId, dateToTimestamp(getLastSynchronizedDate()), + dateToTimestamp(getLastEssentialSynchronizedDate()), countInputsToSynchronize(packageId) ) @@ -38,13 +41,15 @@ class AppSyncDao(private val context: Context) { return cursor } - fun updateLastSynchronizedDate(): Date { + fun updateLastSynchronizedDate(complete: Boolean = true): Date { val now = Date() - this.sharedPreferences.edit() + this.sharedPreferences + .edit() .putLong( - "sync.${AppSync.COLUMN_LAST_SYNC}", - dateToTimestamp(now) ?: -1L + buildLastSynchronizedDatePreferenceKey(complete), + dateToTimestamp(now) + ?: -1L ) .apply() @@ -52,21 +57,37 @@ class AppSyncDao(private val context: Context) { } fun getLastSynchronizedDate(): Date? { - return this.sharedPreferences.getLong( - "sync.${AppSync.COLUMN_LAST_SYNC}", - -1L - ) + return this.sharedPreferences + .getLong( + buildLastSynchronizedDatePreferenceKey(), + -1L + ) + .takeUnless { it == -1L } + .run { fromTimestamp(this) } + } + + fun getLastEssentialSynchronizedDate(): Date? { + return this.sharedPreferences + .getLong( + buildLastSynchronizedDatePreferenceKey(false), + -1L + ) .takeUnless { it == -1L } .run { fromTimestamp(this) } } private fun countInputsToSynchronize(packageId: String): Number { - return FileUtils.getInputsFolder( - context, - packageId - ) + return FileUtils + .getInputsFolder( + context, + packageId + ) .walkTopDown() .filter { it.isFile && it.extension == "json" && it.canRead() } .count() } + + private fun buildLastSynchronizedDatePreferenceKey(complete: Boolean = true): String { + return "sync.${if (complete) AppSync.COLUMN_LAST_SYNC else AppSync.COLUMN_LAST_SYNC_ESSENTIAL}" + } } diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt index 17fbb913..fcd6001a 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncManager.kt @@ -15,18 +15,71 @@ class DataSyncManager private constructor(applicationContext: Context) { private val appSyncDao = AppSyncDao(applicationContext) - private val _lastSynchronizedDate: MutableLiveData = MutableLiveData() - val lastSynchronizedDate: LiveData = _lastSynchronizedDate + private val _lastSynchronizedDate: MutableLiveData> = MutableLiveData( + Pair( + SyncState.FULL, + null + ) + ) + val lastSynchronizedDate: LiveData> = _lastSynchronizedDate - fun updateLastSynchronizedDate() { - _lastSynchronizedDate.postValue(appSyncDao.updateLastSynchronizedDate()) + fun updateLastSynchronizedDate(complete: Boolean = true) { + _lastSynchronizedDate.postValue( + Pair( + if (complete) SyncState.FULL else SyncState.ESSENTIAL, + appSyncDao.updateLastSynchronizedDate(complete) + ) + ) } - fun getLastSynchronizedDate(): Date? { + fun getLastSynchronizedDate(): Pair { val lastSynchronizedDate = appSyncDao.getLastSynchronizedDate() - _lastSynchronizedDate.postValue(lastSynchronizedDate) + val lastEssentialSynchronizedDate = appSyncDao.getLastEssentialSynchronizedDate() - return lastSynchronizedDate + if (lastEssentialSynchronizedDate == null) { + _lastSynchronizedDate.postValue( + Pair( + SyncState.FULL, + lastSynchronizedDate + ) + ) + + return Pair( + SyncState.FULL, + lastSynchronizedDate + ) + } + + if (lastSynchronizedDate == null) { + _lastSynchronizedDate.postValue( + Pair( + SyncState.FULL, + null + ) + ) + + return Pair( + SyncState.FULL, + null + ) + } + + _lastSynchronizedDate.postValue( + Pair( + if (lastSynchronizedDate.after(lastEssentialSynchronizedDate)) SyncState.FULL else SyncState.ESSENTIAL, + if (lastSynchronizedDate.after(lastEssentialSynchronizedDate)) lastSynchronizedDate else lastEssentialSynchronizedDate + ) + ) + + return Pair( + if (lastSynchronizedDate.after(lastEssentialSynchronizedDate)) SyncState.FULL else SyncState.ESSENTIAL, + if (lastSynchronizedDate.after(lastEssentialSynchronizedDate)) lastSynchronizedDate else lastEssentialSynchronizedDate + ) + } + + enum class SyncState { + FULL, + ESSENTIAL } companion object { @@ -41,9 +94,11 @@ class DataSyncManager private constructor(applicationContext: Context) { * * @return The singleton instance of [DataSyncManager]. */ - fun getInstance(applicationContext: Context): DataSyncManager = INSTANCE - ?: synchronized(this) { - INSTANCE ?: DataSyncManager(applicationContext).also { INSTANCE = it } - } + fun getInstance(applicationContext: Context): DataSyncManager = + INSTANCE + ?: synchronized(this) { + INSTANCE + ?: DataSyncManager(applicationContext).also { INSTANCE = it } + } } } diff --git a/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt b/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt index 3ef5066e..f067b483 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/DataSyncViewModel.kt @@ -2,6 +2,7 @@ package fr.geonature.sync.sync import android.app.Application import android.util.Log +import androidx.core.app.NotificationManagerCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -51,7 +52,7 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application _isSyncRunning.postValue(field != null) } - val lastSynchronizedDate: LiveData = dataSyncManager.lastSynchronizedDate + val lastSynchronizedDate: LiveData> = dataSyncManager.lastSynchronizedDate private val _isSyncRunning: MutableLiveData = MutableLiveData(false) val isSyncRunning: LiveData = _isSyncRunning @@ -104,6 +105,11 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application } fun startSync(appSettings: AppSettings) { + Log.i( + TAG, + "starting local data synchronization..." + ) + val dataSyncWorkRequest = OneTimeWorkRequest .Builder(DataSyncWorker::class.java) .addTag(DataSyncWorker.DATA_SYNC_WORKER_TAG) @@ -142,6 +148,10 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application return@launch } + NotificationManagerCompat + .from(getApplication()) + .cancel(DataSyncWorker.SYNC_NOTIFICATION_ID) + workManager .cancelUniqueWork(DataSyncWorker.DATA_SYNC_WORKER_PERIODIC) .await() @@ -207,6 +217,9 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application fun cancelTasks() { workManager.cancelAllWorkByTag(DataSyncWorker.DATA_SYNC_WORKER_TAG) + NotificationManagerCompat + .from(getApplication()) + .cancel(DataSyncWorker.SYNC_NOTIFICATION_ID) } @ExperimentalTime @@ -236,7 +249,7 @@ class DataSyncViewModel(application: Application) : AndroidViewModel(application TimeUnit.SECONDS ) .setBackoffCriteria( - BackoffPolicy.EXPONENTIAL, + BackoffPolicy.LINEAR, (if (withAdditionalData) 1 else 2).toDuration(DurationUnit.MINUTES).inSeconds.toLong(), TimeUnit.SECONDS ) diff --git a/sync/src/main/java/fr/geonature/sync/sync/worker/CheckAuthLoginWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/worker/CheckAuthLoginWorker.kt new file mode 100644 index 00000000..75572747 --- /dev/null +++ b/sync/src/main/java/fr/geonature/sync/sync/worker/CheckAuthLoginWorker.kt @@ -0,0 +1,71 @@ +package fr.geonature.sync.sync.worker + +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.CoroutineWorker +import androidx.work.WorkerParameters +import fr.geonature.sync.MainApplication +import fr.geonature.sync.R +import fr.geonature.sync.auth.AuthManager +import fr.geonature.sync.ui.login.LoginActivity + +/** + * Checks if the current user session is still valid. + * + * @author S. Grimault + */ +class CheckAuthLoginWorker( + appContext: Context, + workerParams: WorkerParameters +) : CoroutineWorker( + appContext, + workerParams +) { + private val authManager: AuthManager = AuthManager.getInstance(applicationContext) + + override suspend fun doWork(): Result { + // not connected: notify user + if (authManager.getAuthLogin() == null) { + with(NotificationManagerCompat.from(applicationContext)) { + cancel(NOTIFICATION_ID) + notify( + NOTIFICATION_ID, + NotificationCompat + .Builder( + applicationContext, + MainApplication.CHANNEL_DATA_SYNCHRONIZATION + ) + .setAutoCancel(true) + .setContentTitle(applicationContext.getText(R.string.notification_data_synchronization_title)) + .setContentText(applicationContext.getString(R.string.sync_error_server_not_connected)) + .setContentIntent( + PendingIntent.getActivity( + applicationContext, + 0, + Intent( + applicationContext, + LoginActivity::class.java + ).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + }, + 0 + ) + ) + .setSmallIcon(R.drawable.ic_sync) + .build() + ) + } + } + + return Result.success() + } + + companion object { + const val CHECK_AUTH_LOGIN_WORKER = "check_auth_login_worker" + const val CHECK_AUTH_LOGIN_WORKER_TAG = "check_auth_login_worker_tag" + const val NOTIFICATION_ID = 4 + } +} \ No newline at end of file diff --git a/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt b/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt index fca140a8..9fb80be8 100644 --- a/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt +++ b/sync/src/main/java/fr/geonature/sync/sync/worker/DataSyncWorker.kt @@ -67,6 +67,23 @@ class DataSyncWorker( // not connected: abort if (authManager.getAuthLogin() == null) { + Log.w( + TAG, + "not connected: abort" + ) + + setForeground( + createForegroundInfo( + createNotification( + DataSyncStatus( + WorkInfo.State.FAILED, + applicationContext.getString(R.string.sync_error_server_not_connected), + ServerStatus.UNAUTHORIZED + ) + ) + ) + ) + return Result.failure( workData( applicationContext.getString(R.string.sync_error_server_not_connected), @@ -188,7 +205,13 @@ class DataSyncWorker( .from(applicationContext) .cancel(SYNC_NOTIFICATION_ID) - dataSyncManager.updateLastSynchronizedDate() + dataSyncManager.updateLastSynchronizedDate( + inputData.getBoolean( + INPUT_WITH_ADDITIONAL_DATA, + true + ) + ) + return Result.success(workData(applicationContext.getString(R.string.sync_data_succeeded))) } @@ -1061,7 +1084,7 @@ class DataSyncWorker( const val DATA_SYNC_WORKER_PERIODIC_ESSENTIAL = "data_sync_worker_periodic_essential" const val DATA_SYNC_WORKER_TAG = "data_sync_worker_tag" - private const val SYNC_NOTIFICATION_ID = 3 + const val SYNC_NOTIFICATION_ID = 3 private const val INPUT_USERS_MENU_ID = "usersMenuId" private const val INPUT_TAXREF_LIST_ID = "taxrefListId" diff --git a/sync/src/main/java/fr/geonature/sync/ui/home/DataSyncView.kt b/sync/src/main/java/fr/geonature/sync/ui/home/DataSyncView.kt index 1e2105c4..0ae1d2d1 100644 --- a/sync/src/main/java/fr/geonature/sync/ui/home/DataSyncView.kt +++ b/sync/src/main/java/fr/geonature/sync/ui/home/DataSyncView.kt @@ -11,6 +11,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.res.ResourcesCompat import androidx.work.WorkInfo import fr.geonature.sync.R +import fr.geonature.sync.sync.DataSyncManager import java.util.Date /** @@ -22,6 +23,7 @@ class DataSyncView : ConstraintLayout { private lateinit var iconStatus: TextView private lateinit var textViewMessage: TextView + private lateinit var textViewLastSynchronizedDateTitle: TextView private lateinit var textViewLastSynchronizedDate: TextView private val stateAnimation = AlphaAnimation( @@ -121,13 +123,14 @@ class DataSyncView : ConstraintLayout { textViewMessage.text = text } - fun setLastSynchronizedDate(lastSynchronized: Date?) { - val formatLastSynchronizedDate = if (lastSynchronized == null) context.getString(R.string.sync_last_synchronization_never) + fun setLastSynchronizedDate(lastSynchronized: Pair) { + val formatLastSynchronizedDate = if (lastSynchronized.second == null) context.getString(R.string.sync_last_synchronization_never) else DateFormat.format( context.getString(R.string.sync_last_synchronization_date), - lastSynchronized + lastSynchronized.second ) + textViewLastSynchronizedDateTitle.text = context.getText(if (lastSynchronized.first == DataSyncManager.SyncState.FULL) R.string.sync_last_synchronization_full else R.string.sync_last_synchronization) textViewLastSynchronizedDate.text = formatLastSynchronizedDate } @@ -143,6 +146,7 @@ class DataSyncView : ConstraintLayout { iconStatus = findViewById(android.R.id.icon) textViewMessage = findViewById(android.R.id.message) + textViewLastSynchronizedDateTitle = findViewById(android.R.id.text1) textViewLastSynchronizedDate = findViewById(android.R.id.text2) } } diff --git a/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt b/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt index 673fd620..cc996f6b 100644 --- a/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt +++ b/sync/src/main/java/fr/geonature/sync/ui/home/HomeActivity.kt @@ -264,6 +264,16 @@ class HomeActivity : AppCompatActivity() { AuthLoginViewModel.Factory { AuthLoginViewModel(application) }) .get(AuthLoginViewModel::class.java) .also { vm -> + vm.checkAuthLogin().observeOnce(this@HomeActivity) { + if (checkGeoNatureSettings() && it == null) { + Log.i( + TAG, + "not connected, redirect to LoginActivity" + ) + + startSyncResultLauncher.launch(LoginActivity.newIntent(this@HomeActivity)) + } + } vm.isLoggedIn.observe(this@HomeActivity, { this@HomeActivity.isLoggedIn = it @@ -340,7 +350,7 @@ class HomeActivity : AppCompatActivity() { if (it.serverStatus == UNAUTHORIZED) { Log.i( TAG, - "not connected, redirect to LoginActivity" + "not connected (HTTP error code: 401), redirect to LoginActivity" ) Toast diff --git a/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt b/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt index 50400bec..42ac29a9 100644 --- a/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt +++ b/sync/src/main/java/fr/geonature/sync/ui/login/LoginActivity.kt @@ -27,6 +27,7 @@ import fr.geonature.sync.settings.AppSettingsViewModel class LoginActivity : AppCompatActivity() { private lateinit var authLoginViewModel: AuthLoginViewModel + private var appSettings: AppSettings? = null private var content: ConstraintLayout? = null @@ -40,30 +41,26 @@ class LoginActivity : AppCompatActivity() { setContentView(R.layout.activity_login) - authLoginViewModel = ViewModelProvider( - this, - AuthLoginViewModel.Factory { AuthLoginViewModel(application) } - ).get(AuthLoginViewModel::class.java) + authLoginViewModel = ViewModelProvider(this, + AuthLoginViewModel.Factory { AuthLoginViewModel(application) }) + .get(AuthLoginViewModel::class.java) .apply { - loginFormState.observe( - this@LoginActivity, + loginFormState.observe(this@LoginActivity, Observer { - val loginState = it ?: return@Observer + val loginState = it + ?: return@Observer // disable login button unless both username / password is valid buttonLogin?.isEnabled = loginState.isValid && appSettings != null - editTextUsername?.error = - if (loginState.usernameError == null) null else getString(loginState.usernameError) - editTextPassword?.error = - if (loginState.passwordError == null) null else getString(loginState.passwordError) - } - ) + editTextUsername?.error = if (loginState.usernameError == null) null else getString(loginState.usernameError) + editTextPassword?.error = if (loginState.passwordError == null) null else getString(loginState.passwordError) + }) - loginResult.observe( - this@LoginActivity, + loginResult.observe(this@LoginActivity, Observer { - val loginResult = it ?: return@Observer + val loginResult = it + ?: return@Observer progress?.visibility = View.GONE @@ -81,8 +78,7 @@ class LoginActivity : AppCompatActivity() { // Complete and destroy login activity once successful setResult(RESULT_OK) finish() - } - ) + }) } content = findViewById(R.id.content) @@ -146,16 +142,16 @@ class LoginActivity : AppCompatActivity() { } private fun loadAppSettings() { - ViewModelProvider( - this, + ViewModelProvider(this, fr.geonature.commons.settings.AppSettingsViewModel.Factory { AppSettingsViewModel( application ) - } - ).get(AppSettingsViewModel::class.java) + }) + .get(AppSettingsViewModel::class.java) .also { vm -> - vm.loadAppSettings() + vm + .loadAppSettings() .observeOnce(this) { if (it == null) { makeSnackbar( @@ -163,8 +159,8 @@ class LoginActivity : AppCompatActivity() { R.string.snackbar_settings_not_found, vm.getAppSettingsFilename() ) - )?.addCallback( - object : + ) + ?.addCallback(object : BaseTransientBottomBar.BaseCallback() { override fun onDismissed( transientBottomBar: Snackbar?, @@ -178,8 +174,7 @@ class LoginActivity : AppCompatActivity() { setResult(RESULT_CANCELED) finish() } - } - ) + }) ?.show() } else { appSettings = it @@ -192,7 +187,8 @@ class LoginActivity : AppCompatActivity() { username: String, password: String ) { - val appSettings = appSettings ?: return + val appSettings = appSettings + ?: return editTextPassword?.also { hideSoftKeyboard(it) @@ -207,19 +203,20 @@ class LoginActivity : AppCompatActivity() { } private fun showToast( - @StringRes - messageResourceId: Int + @StringRes messageResourceId: Int ) { - Toast.makeText( - applicationContext, - messageResourceId, - Toast.LENGTH_LONG - ) + Toast + .makeText( + applicationContext, + messageResourceId, + Toast.LENGTH_LONG + ) .show() } private fun makeSnackbar(text: CharSequence): Snackbar? { - val view = content ?: return null + val view = content + ?: return null return Snackbar.make( view, diff --git a/sync/src/main/res/values-fr/strings.xml b/sync/src/main/res/values-fr/strings.xml index 97ce2459..64288f69 100644 --- a/sync/src/main/res/values-fr/strings.xml +++ b/sync/src/main/res/values-fr/strings.xml @@ -10,6 +10,7 @@ Se déconnecter Dernière synchronisation + Dernière synchronisation complète le EEE dd MMM yyyy à kk:mm:ss Jamais Synchronisation en cours… diff --git a/sync/src/main/res/values/strings.xml b/sync/src/main/res/values/strings.xml index 8e643b36..49c13ee7 100644 --- a/sync/src/main/res/values/strings.xml +++ b/sync/src/main/res/values/strings.xml @@ -13,6 +13,7 @@ Sign out Last synchronization + Last full synchronization EEE dd MMM yyyy, kk:mm:ss Never Starting local data synchronization… From 9bd02734ce1143a0649c6467002aac092228bf81 Mon Sep 17 00:00:00 2001 From: "S. Grimault" Date: Mon, 24 May 2021 15:05:11 +0200 Subject: [PATCH 2/2] chore: 1.1.9 release --- sync/build.gradle | 2 +- sync/version.properties | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sync/build.gradle b/sync/build.gradle index f2035cf8..28180ef3 100644 --- a/sync/build.gradle +++ b/sync/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: "kotlin-kapt" -version = "1.1.8" +version = "1.1.9" android { compileSdkVersion 29 diff --git a/sync/version.properties b/sync/version.properties index de7e35f4..8f0a2949 100644 --- a/sync/version.properties +++ b/sync/version.properties @@ -1,2 +1,2 @@ -#Sun May 02 14:35:10 CEST 2021 -VERSION_CODE=2870 +#Mon May 24 15:03:05 CEST 2021 +VERSION_CODE=2910