From 9188c7790fe1c4d8e68f0cd0a99693a69505c367 Mon Sep 17 00:00:00 2001 From: wrov Date: Thu, 27 Feb 2020 18:35:40 +0100 Subject: [PATCH] Multiplayer Notification Fix for random Termination (#2024) * Reworked Notification service to remove Nullpointer crashes due to non-final static variables being null. Instead moved all state to Worker specific architecture. Also switched to Android based localization because core-based one can be unavailable to worker. * Added missing blanks * Added more internationalization Added missing androidX declaration (neccessary for newer Gradle versions) Updated Gradle for new Android Studio Version * Optimization * Removed missing translations error --- .gitignore | 1 - .../translationsByLanguage/German.properties | 9 -- .../template.properties | 9 -- android/build.gradle | 5 +- android/gradle.properties | 2 + android/res/values-de/strings.xml | 17 +++ android/res/values-fr-rFR/strings.xml | 9 ++ android/res/values/strings.xml | 13 ++ .../unciv/app/MultiplayerTurnCheckWorker.kt | 132 +++++++++--------- build.gradle | 4 +- gradle/wrapper/gradle-wrapper.properties | 4 +- 11 files changed, 116 insertions(+), 89 deletions(-) create mode 100644 android/gradle.properties create mode 100644 android/res/values-de/strings.xml create mode 100644 android/res/values-fr-rFR/strings.xml diff --git a/.gitignore b/.gitignore index 9e166f4e..537f08f0 100644 --- a/.gitignore +++ b/.gitignore @@ -123,7 +123,6 @@ Thumbs.db !/ios-moe/xcode/*.xcodeproj/xcshareddata !/ios-moe/xcode/*.xcodeproj/project.pbxproj /ios-moe/xcode/native/ -gradle.properties SaveFiles/ android/android-release.apk android/assets/GameSettings.json diff --git a/android/assets/jsons/translationsByLanguage/German.properties b/android/assets/jsons/translationsByLanguage/German.properties index a4051e5f..29dcb8e8 100644 --- a/android/assets/jsons/translationsByLanguage/German.properties +++ b/android/assets/jsons/translationsByLanguage/German.properties @@ -1747,15 +1747,6 @@ Haka War Dance = Haka-Kriegstanz Rejuvenation = Verjüngung All healing effects doubled = Alle Heilungseffekte verdoppelt # Multiplayer Turn Checker Service -An error has occured = Ein Fehler ist aufgetreten -Multiplayer turn notifier service terminated = Multiplayer Zug Benachrichtigungsdienst wurde beendet -Your friends are waiting on your turn. = Deine Freunde warten auf deinen Zug. -Unciv - It's your turn! = Unciv - Du bist am Zug! -Unciv will inform you when it's your turn in multiplayer. = Unciv wird dich benachrichtigen, wenn du im Multiplayer am Zug bist. -Last online turn check: [lastTimeChecked] = Letzter online Zug Check: [lastTimeChecked] -Checks ca. every [checkPeriod] minute(s) when Internet available. = Prüft etwa alle [checkPeriod] Minute(n) wenn Internet vorhanden. -Configurable in Unciv options menu. = Konfigurierbar im Unciv Optionsmenü. -Unciv multiplayer turn notifier running = Unciv Multiplayer Zug Benachrichtiger läuft. Multiplayer options = Multiplayer Einstellungen Enable out-of-game turn notifications = Aktiviere Zug Benachrichtigungen außerhalb des Spiels Time between turn checks out-of-game (in minutes) = Intervall zwischen Zug Prüfungen (in Minuten) diff --git a/android/assets/jsons/translationsByLanguage/template.properties b/android/assets/jsons/translationsByLanguage/template.properties index 3a2b15e2..d48a9ecd 100644 --- a/android/assets/jsons/translationsByLanguage/template.properties +++ b/android/assets/jsons/translationsByLanguage/template.properties @@ -1743,15 +1743,6 @@ Haka War Dance = Rejuvenation = All healing effects doubled = # Multiplayer Turn Checker Service -An error has occured = -Multiplayer turn notifier service terminated = -Your friends are waiting on your turn. = -Unciv - It's your turn! = -Unciv will inform you when it's your turn in multiplayer. = -Last online turn check: [lastTimeChecked] = -Checks ca. every [checkPeriod] minute(s) when Internet available. = -Configurable in Unciv options menu. = -Unciv multiplayer turn notifier running = Multiplayer options = Enable out-of-game turn notifications = Time between turn checks out-of-game (in minutes) = diff --git a/android/build.gradle b/android/build.gradle index 1ef8549d..1719f7c1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -62,6 +62,9 @@ android { } } } + lintOptions { + disable 'MissingTranslation' + } } @@ -123,7 +126,7 @@ task run(type: Exec) { dependencies { implementation 'androidx.core:core:1.2.0' - implementation "androidx.work:work-runtime-ktx:2.3.1" + implementation "androidx.work:work-runtime-ktx:2.3.2" } // sets up the Android Eclipse project, using the old Ant based build. diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 00000000..d015431a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,2 @@ +android.useAndroidX=true +android.enableJetifier=true \ No newline at end of file diff --git a/android/res/values-de/strings.xml b/android/res/values-de/strings.xml new file mode 100644 index 00000000..e4c170cc --- /dev/null +++ b/android/res/values-de/strings.xml @@ -0,0 +1,17 @@ + + + UnCiv + Unciv - Du bist am Zug! + Deine Freunde warten auf deinen Zug. + Ein Fehler ist aufgetreten + Multiplayer Zug Benachrichtigungsdienst wurde beendet + Letzter online Zug Check: + Unciv wird dich benachrichtigen, wenn du im Multiplayer am Zug bist. + Prüft etwa alle + Minute(n) wenn Internet vorhanden. + Konfigurierbar im Unciv Optionsmenü. + Unciv Multiplayer Zugprüfer Ereignis + Informiert dich, wenn du im Multiplayer am Zug bist. + Unciv Multiplayer Zugprüfer Persistenter Status + Permanent angezeigte Benachrichtigung, welche dich über die Hintergrundaktivität des Dienstes informiert. + \ No newline at end of file diff --git a/android/res/values-fr-rFR/strings.xml b/android/res/values-fr-rFR/strings.xml new file mode 100644 index 00000000..d8567e77 --- /dev/null +++ b/android/res/values-fr-rFR/strings.xml @@ -0,0 +1,9 @@ + + + UnCiv + Unciv - C\'est à vous ! + Vos amis attendent votre action. + Service de notification du tour multijoueur terminé + Une erreur est survenue + Configurable dans le menu des options de Unciv + \ No newline at end of file diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index db54450e..b5b5a4ef 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -1,4 +1,17 @@ UnCiv + Unciv - It\'s your turn! + Your friends are waiting on your turn. + An error has occurred + Multiplayer turn notifier service terminated + Last online turn check: + Unciv will inform you when it\'s your turn in multiplayer. + Checks ca. every + minute(s) when Internet available. + Configurable in Unciv options menu. + Unciv Multiplayer Turn Checker Alert + Informs you when it\'s your turn in multiplayer. + Unciv Multiplayer Turn Checker Persistent Status + Shown constantly to inform you about background checking. diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 2f2162f0..925736e9 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -10,12 +10,10 @@ import android.os.Build import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE import androidx.core.app.NotificationManagerCompat -import androidx.core.content.ContextCompat.getSystemService import androidx.work.* import com.badlogic.gdx.backends.android.AndroidApplication import com.unciv.logic.GameInfo import com.unciv.models.metadata.GameSettings -import com.unciv.models.translations.tr import com.unciv.ui.worldscreen.mainmenu.OnlineMultiplayer import java.util.* import java.util.concurrent.TimeUnit @@ -37,16 +35,15 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame private const val NOTIFICATION_CHANNEL_ID_INFO = "UNCIV_NOTIFICATION_CHANNEL_INFO" private const val NOTIFICATION_CHANNEL_ID_SERVICE = "UNCIV_NOTIFICATION_CHANNEL_SERVICE_02" + private const val FAIL_COUNT = "FAIL_COUNT" + private const val GAME_ID = "GAME_ID" + private const val USER_ID = "USER_ID" + private const val CONFIGURED_DELAY = "CONFIGURED_DELAY" + private const val PERSISTENT_NOTIFICATION_ENABLED = "PERSISTENT_NOTIFICATION_ENABLED" - // These fields need to be volatile because they are set by main thread but later accessed by worker thread. - // Classes used here need to be primitive or internally synchronized to avoid visibility issues. - @Volatile private var failCount = 0 - @Volatile private var gameId = "" - @Volatile private var userId = "" - @Volatile private var configuredDelay = 5 - @Volatile private var persistentNotificationEnabled = true + fun enqueue(appContext: Context, + delayInMinutes: Int, inputData: Data) { - fun enqueue(appContext: Context, delayInMinutes: Int) { val constraints = Constraints.Builder() // If no internet is available, worker waits before becoming active. .setRequiredNetworkType(NetworkType.CONNECTED) @@ -56,6 +53,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame .setConstraints(constraints) .setInitialDelay(delayInMinutes.toLong(), TimeUnit.MINUTES) .addTag(WORK_TAG) + .setInputData(inputData) .build() WorkManager.getInstance(appContext).enqueue(checkTurnWork) @@ -70,8 +68,8 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame */ fun createNotificationChannelInfo(appContext: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "Unciv Multiplayer Turn Checker Alert" - val descriptionText = "Informs you when it's your turn in multiplayer." + val name = appContext.resources.getString(R.string.Notify_ChannelInfo_Short) + val descriptionText = appContext.resources.getString(R.string.Notify_ChannelInfo_Long) val importance = NotificationManager.IMPORTANCE_HIGH val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID_INFO, name, importance) mChannel.description = descriptionText @@ -92,8 +90,8 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame */ fun createNotificationChannelService(appContext: Context) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val name = "Unciv Multiplayer Turn Checker Persistent Status" - val descriptionText = "Shown constantly to inform you about background checking." + val name = appContext.resources.getString(R.string.Notify_ChannelService_Short) + val descriptionText = appContext.resources.getString(R.string.Notify_ChannelService_Long) val importance = NotificationManager.IMPORTANCE_MIN val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID_SERVICE, name, importance) mChannel.setShowBadge(false) @@ -110,29 +108,28 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame * It is not technically necessary for the Worker, since it is not a Service. */ fun showPersistentNotification(appContext: Context, lastTimeChecked: String, checkPeriod: String) { - if (persistentNotificationEnabled) { - val pendingIntent: PendingIntent = - Intent(appContext, AndroidLauncher::class.java).let { notificationIntent -> - PendingIntent.getActivity(appContext, 0, notificationIntent, 0) - } + val pendingIntent: PendingIntent = + Intent(appContext, AndroidLauncher::class.java).let { notificationIntent -> + PendingIntent.getActivity(appContext, 0, notificationIntent, 0) + } - val notification: NotificationCompat.Builder = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID_SERVICE) - .setPriority(NotificationManagerCompat.IMPORTANCE_MIN) // it's only a status - .setContentTitle(("Last online turn check: [$lastTimeChecked]").tr()) - .setStyle(NotificationCompat.BigTextStyle() - .bigText("Unciv will inform you when it's your turn in multiplayer.".tr() + " " + - "Checks ca. every [$checkPeriod] minute(s) when Internet available.".tr() - + " " + "Configurable in Unciv options menu.".tr())) - .setSmallIcon(R.drawable.uncivicon2) - .setContentIntent(pendingIntent) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setOnlyAlertOnce(true) - .setOngoing(true) - .setShowWhen(false) + val notification: NotificationCompat.Builder = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID_SERVICE) + .setPriority(NotificationManagerCompat.IMPORTANCE_MIN) // it's only a status + .setContentTitle(appContext.resources.getString(R.string.Notify_Persist_Short) + " " + lastTimeChecked) + .setStyle(NotificationCompat.BigTextStyle() + .bigText(appContext.resources.getString(R.string.Notify_Persist_Long_P1) + " " + + appContext.resources.getString(R.string.Notify_Persist_Long_P2) + " " + checkPeriod + " " + + appContext.resources.getString(R.string.Notify_Persist_Long_P3) + + " " + appContext.resources.getString(R.string.Notify_Persist_Long_P4))) + .setSmallIcon(R.drawable.uncivicon2) + .setContentIntent(pendingIntent) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setOnlyAlertOnce(true) + .setOngoing(true) + .setShowWhen(false) - with(NotificationManagerCompat.from(appContext)) { - notify(NOTIFICATION_ID_INFO, notification.build()) - } + with(NotificationManagerCompat.from(appContext)) { + notify(NOTIFICATION_ID_INFO, notification.build()) } } @@ -142,11 +139,11 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame PendingIntent.getActivity(applicationContext, 0, notificationIntent, 0) } - val contentTitle = "Unciv - It's your turn!".tr() + val contentTitle = applicationContext.resources.getString(R.string.Notify_YourTurn_Short) val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO) .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH) // people are waiting! .setContentTitle(contentTitle) - .setContentText("Your friends are waiting on your turn.".tr()) + .setContentText(applicationContext.resources.getString(R.string.Notify_YourTurn_Long)) .setTicker(contentTitle) // without at least vibrate, some Android versions don't show a heads-up notification .setDefaults(DEFAULT_VIBRATE) @@ -166,15 +163,16 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame // May be useful to remind a player that he forgot to complete his turn. notifyUserAboutTurn(applicationContext) } else { - gameId = gameInfo.gameId - userId = settings.userId - configuredDelay = settings.multiplayerTurnCheckerDelayInMinutes - persistentNotificationEnabled = settings.multiplayerTurnCheckerPersistentNotificationEnabled + val inputData = workDataOf(Pair(FAIL_COUNT, 0), Pair(GAME_ID, gameInfo.gameId), + Pair(USER_ID, settings.userId), Pair(CONFIGURED_DELAY, settings.multiplayerTurnCheckerDelayInMinutes), + Pair(PERSISTENT_NOTIFICATION_ENABLED, settings.multiplayerTurnCheckerPersistentNotificationEnabled)) - showPersistentNotification(applicationContext, - "—", settings.multiplayerTurnCheckerDelayInMinutes.toString()) + if (settings.multiplayerTurnCheckerPersistentNotificationEnabled) { + showPersistentNotification(applicationContext, + "—", settings.multiplayerTurnCheckerDelayInMinutes.toString()) + } // Initial check always happens after a minute, ignoring delay config. Better user experience this way. - enqueue(applicationContext, 1) + enqueue(applicationContext, 1, inputData) } } @@ -205,47 +203,51 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame override fun doWork(): Result { try { - val latestGame = OnlineMultiplayer().tryDownloadGame(gameId) - if (latestGame.currentPlayerCiv.playerId == userId) { + val latestGame = OnlineMultiplayer().tryDownloadGame(inputData.getString(GAME_ID)!!) + if (latestGame.currentPlayerCiv.playerId == inputData.getString(USER_ID)!!) { notifyUserAboutTurn(applicationContext) with(NotificationManagerCompat.from(applicationContext)) { cancel(NOTIFICATION_ID_SERVICE) } } else { - enqueue(applicationContext, configuredDelay) - updatePersistentNotification() + updatePersistentNotification(inputData) + // We have to reset the fail counter since no exception appeared + val inputDataFailReset = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, 0).build() + enqueue(applicationContext, inputData.getInt(CONFIGURED_DELAY, 5), inputDataFailReset) } - failCount = 0 } catch (ex: Exception) { - if (failCount++ > 3) { + val failCount = inputData.getInt(FAIL_COUNT, 0) + if (failCount > 3) { showErrorNotification() with(NotificationManagerCompat.from(applicationContext)) { cancel(NOTIFICATION_ID_SERVICE) } - failCount = 0 // Otherwise the notification service would be forever stuck in error mode. return Result.failure() } else { // If check fails, retry in one minute. // Makes sense, since checks only happen if Internet is available in principle. // Therefore a failure means either a problem with the GameInfo or with Dropbox. - enqueue(applicationContext, 1) - updatePersistentNotification() + val inputDataFailIncrease = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, failCount + 1).build() + enqueue(applicationContext, 1, inputDataFailIncrease) + // Persistent Notification is not updated, because user may think check succeed. } } return Result.success() } - private fun updatePersistentNotification() { - val cal = GregorianCalendar.getInstance() - val hour = cal.get(GregorianCalendar.HOUR_OF_DAY).toString() - var minute = cal.get(GregorianCalendar.MINUTE).toString() - if (minute.length == 1) { - minute = "0$minute" - } - val displayTime = "$hour:$minute" + private fun updatePersistentNotification(inputData: Data) { + if (inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)) { + val cal = GregorianCalendar.getInstance() + val hour = cal.get(GregorianCalendar.HOUR_OF_DAY).toString() + var minute = cal.get(GregorianCalendar.MINUTE).toString() + if (minute.length == 1) { + minute = "0$minute" + } + val displayTime = "$hour:$minute" - showPersistentNotification(applicationContext, displayTime, - configuredDelay.toString()) + showPersistentNotification(applicationContext, displayTime, + inputData.getInt(CONFIGURED_DELAY, 5).toString()) + } } private fun showErrorNotification() { @@ -256,8 +258,8 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO) .setPriority(NotificationManagerCompat.IMPORTANCE_DEFAULT) // No direct user action expected - .setContentTitle("An error has occured".tr()) - .setContentText("Multiplayer turn notifier service terminated".tr()) + .setContentTitle(applicationContext.resources.getString(R.string.Notify_Error_Short)) + .setContentText(applicationContext.resources.getString(R.string.Notify_Error_Long)) .setSmallIcon(R.drawable.uncivicon2) // without at least vibrate, some Android versions don't show a heads-up notification .setDefaults(DEFAULT_VIBRATE) diff --git a/build.gradle b/build.gradle index 797574f0..eed64dc6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ buildscript { - ext.kotlinVersion = '1.3.50' + ext.kotlinVersion = '1.3.61' repositories { // Chinese mirrors for quicker loading for chinese devs - uncomment if you're chinese @@ -18,7 +18,7 @@ buildscript { dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" classpath 'de.richsource.gradle.plugins:gwt-gradle-plugin:0.6' - classpath 'com.android.tools.build:gradle:3.5.3' + classpath 'com.android.tools.build:gradle:3.6.0' classpath 'com.mobidevelop.robovm:robovm-gradle-plugin:2.3.1' // This is for wrapping the .jar file into a standalone executable diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 56e0614b..92a5f55b 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Aug 24 11:02:13 CST 2019 +#Thu Feb 27 10:30:48 CET 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip