From 1df0c408aabbb58453111199a7c8d99aaa04cf69 Mon Sep 17 00:00:00 2001 From: wrov Date: Mon, 2 Mar 2020 05:44:53 +0100 Subject: [PATCH] Fixed Multiplayer Turn Notifier periodically failing with error notification. (#2054) --- android/AndroidManifest.xml | 6 +- android/res/values-de/strings.xml | 7 ++- .../{values-fr-rFR => values-fr}/strings.xml | 0 android/res/values/strings.xml | 5 +- .../com/unciv/app/CopyToClipboardReceiver.kt | 19 ++++++ .../unciv/app/MultiplayerTurnCheckWorker.kt | 61 ++++++++++++------- core/src/com/unciv/logic/GameSaver.kt | 11 ++++ .../unciv/ui/worldscreen/mainmenu/DropBox.kt | 16 ++++- 8 files changed, 96 insertions(+), 29 deletions(-) rename android/res/{values-fr-rFR => values-fr}/strings.xml (100%) create mode 100644 android/src/com/unciv/app/CopyToClipboardReceiver.kt diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index 50f6cc6a..d3d8fc51 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -1,5 +1,6 @@ @@ -17,13 +18,14 @@ android:launchMode="singleTask" android:label="@string/app_name" android:screenOrientation="userLandscape" - android:configChanges="keyboard|keyboardHidden|orientation|screenSize"> + android:configChanges="keyboard|keyboardHidden|orientation|screenSize" + tools:ignore="LockedOrientationActivity"> - + diff --git a/android/res/values-de/strings.xml b/android/res/values-de/strings.xml index e4c170cc..f6d65c5f 100644 --- a/android/res/values-de/strings.xml +++ b/android/res/values-de/strings.xml @@ -4,8 +4,8 @@ Unciv - Du bist am Zug! Deine Freunde warten auf deinen Zug. Ein Fehler ist aufgetreten - Multiplayer Zug Benachrichtigungsdienst wurde beendet - Letzter online Zug Check: + Multiplayer Zug Benachrichtigungsdienst wurde beendet. + Letzter online Zugcheck: Unciv wird dich benachrichtigen, wenn du im Multiplayer am Zug bist. Prüft etwa alle Minute(n) wenn Internet vorhanden. @@ -14,4 +14,7 @@ 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. + Stacktraces in Zwischenablage kopiert. + Kopiere Stacktraces in Zwischenablage + Fehler, wiederhole… \ No newline at end of file diff --git a/android/res/values-fr-rFR/strings.xml b/android/res/values-fr/strings.xml similarity index 100% rename from android/res/values-fr-rFR/strings.xml rename to android/res/values-fr/strings.xml diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml index b5b5a4ef..57614a86 100644 --- a/android/res/values/strings.xml +++ b/android/res/values/strings.xml @@ -4,7 +4,7 @@ Unciv - It\'s your turn! Your friends are waiting on your turn. An error has occurred - Multiplayer turn notifier service terminated + Multiplayer turn notifier service terminated. Last online turn check: Unciv will inform you when it\'s your turn in multiplayer. Checks ca. every @@ -14,4 +14,7 @@ Informs you when it\'s your turn in multiplayer. Unciv Multiplayer Turn Checker Persistent Status Shown constantly to inform you about background checking. + Stacktraces copied to clipboard. + Copy stacktrace to clipboard + failed, retrying… diff --git a/android/src/com/unciv/app/CopyToClipboardReceiver.kt b/android/src/com/unciv/app/CopyToClipboardReceiver.kt new file mode 100644 index 00000000..e4697325 --- /dev/null +++ b/android/src/com/unciv/app/CopyToClipboardReceiver.kt @@ -0,0 +1,19 @@ +package com.unciv.app + +import android.content.* +import android.widget.Toast +import com.badlogic.gdx.backends.android.AndroidApplication + +/** + * This Receiver can be called from an Action on the error Notification shown by MultiplayerTurnCheckWorker. + * It copies the text given to it to clipboard and then shows a Toast. + * It's intended to help find out why the Turn Notifier failed. + */ +class CopyToClipboardReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val clipboard: ClipboardManager = context.getSystemService(AndroidApplication.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText("exception", intent.getStringExtra(MultiplayerTurnCheckWorker.CLIPBOARD_EXTRA)) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, context.resources.getString(R.string.Notify_Error_StackTrace_Toast), Toast.LENGTH_SHORT).show() + } +} \ No newline at end of file diff --git a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt index 925736e9..231254a9 100644 --- a/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt +++ b/android/src/com/unciv/app/MultiplayerTurnCheckWorker.kt @@ -15,6 +15,9 @@ import com.badlogic.gdx.backends.android.AndroidApplication import com.unciv.logic.GameInfo import com.unciv.models.metadata.GameSettings import com.unciv.ui.worldscreen.mainmenu.OnlineMultiplayer +import java.io.PrintWriter +import java.io.StringWriter +import java.io.Writer import java.util.* import java.util.concurrent.TimeUnit @@ -24,6 +27,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame companion object { const val WORK_TAG = "UNCIV_MULTIPLAYER_TURN_CHECKER_WORKER" + const val CLIPBOARD_EXTRA = "CLIPBOARD_STRING" const val NOTIFICATION_ID_SERVICE = 1 const val NOTIFICATION_ID_INFO = 2 @@ -202,60 +206,72 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame } override fun doWork(): Result { + val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true) + val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5) try { - val latestGame = OnlineMultiplayer().tryDownloadGame(inputData.getString(GAME_ID)!!) - if (latestGame.currentPlayerCiv.playerId == inputData.getString(USER_ID)!!) { + val currentTurnPlayer = OnlineMultiplayer().tryDownloadCurrentTurnCiv(inputData.getString(GAME_ID)!!) + if (currentTurnPlayer.playerId == inputData.getString(USER_ID)!!) { notifyUserAboutTurn(applicationContext) with(NotificationManagerCompat.from(applicationContext)) { cancel(NOTIFICATION_ID_SERVICE) } } else { - updatePersistentNotification(inputData) + if (showPersistNotific) { 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) + enqueue(applicationContext, configuredDelay, inputDataFailReset) } } catch (ex: Exception) { val failCount = inputData.getInt(FAIL_COUNT, 0) if (failCount > 3) { - showErrorNotification() + showErrorNotification(getStackTraceString(ex)) with(NotificationManagerCompat.from(applicationContext)) { cancel(NOTIFICATION_ID_SERVICE) } return Result.failure() } else { + if (showPersistNotific) { showPersistentNotification(applicationContext, + applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) } // 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. 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(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, - inputData.getInt(CONFIGURED_DELAY, 5).toString()) - } + private fun getStackTraceString(ex: Exception): String { + val writer: Writer = StringWriter() + ex.printStackTrace(PrintWriter(writer)) + return writer.toString() } - private fun showErrorNotification() { - val pendingIntent: PendingIntent = + private fun updatePersistentNotification(inputData: Data) { + 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, + inputData.getInt(CONFIGURED_DELAY, 5).toString()) + } + + private fun showErrorNotification(stackTraceString: String) { + val pendingLaunchGameIntent: PendingIntent = Intent(applicationContext, AndroidLauncher::class.java).let { notificationIntent -> PendingIntent.getActivity(applicationContext, 0, notificationIntent, 0) } + val pendingCopyClipboardIntent: PendingIntent = + Intent(applicationContext, CopyToClipboardReceiver::class.java).putExtra(CLIPBOARD_EXTRA, stackTraceString) + .let { notificationIntent -> PendingIntent.getBroadcast(applicationContext,0, notificationIntent, 0) + } + val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO) .setPriority(NotificationManagerCompat.IMPORTANCE_DEFAULT) // No direct user action expected .setContentTitle(applicationContext.resources.getString(R.string.Notify_Error_Short)) @@ -264,9 +280,10 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame // without at least vibrate, some Android versions don't show a heads-up notification .setDefaults(DEFAULT_VIBRATE) .setLights(Color.YELLOW, 300, 100) - .setContentIntent(pendingIntent) + .setContentIntent(pendingLaunchGameIntent) .setCategory(NotificationCompat.CATEGORY_ERROR) .setOngoing(false) + .addAction(0, applicationContext.resources.getString(R.string.Notify_Error_CopyAction), pendingCopyClipboardIntent) with(NotificationManagerCompat.from(applicationContext)) { notify(NOTIFICATION_ID_INFO, notification.build()) diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index 28d2a1fc..ad413c98 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -3,6 +3,7 @@ package com.unciv.logic import com.badlogic.gdx.Gdx import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.utils.Json +import com.unciv.logic.civilization.CivilizationInfo import com.unciv.models.metadata.GameSettings import com.unciv.ui.utils.ImageGetter import java.io.File @@ -95,5 +96,15 @@ class GameSaver { } } + + /** + * Returns current turn's player from GameInfo JSON-String for multiplayer. + * Does not initialize transitive GameInfo data. + * It is therefore stateless and save to call for Multiplayer Turn Notifier, unlike gameInfoFromString(). + */ + fun currentTurnCivFromString(gameData: String): CivilizationInfo { + val game = json().fromJson(GameInfo::class.java, gameData) + return game.getCivilization(game.currentPlayer) + } } diff --git a/core/src/com/unciv/ui/worldscreen/mainmenu/DropBox.kt b/core/src/com/unciv/ui/worldscreen/mainmenu/DropBox.kt index 0ea54335..f80896c1 100644 --- a/core/src/com/unciv/ui/worldscreen/mainmenu/DropBox.kt +++ b/core/src/com/unciv/ui/worldscreen/mainmenu/DropBox.kt @@ -2,6 +2,7 @@ package com.unciv.ui.worldscreen.mainmenu import com.unciv.logic.GameInfo import com.unciv.logic.GameSaver +import com.unciv.logic.civilization.CivilizationInfo import com.unciv.ui.saves.Gzip import java.io.BufferedReader import java.io.DataOutputStream @@ -9,7 +10,7 @@ import java.io.InputStream import java.io.InputStreamReader import java.net.HttpURLConnection import java.net.URL -import java.nio.charset.StandardCharsets +import java.nio.charset.Charset class DropBox { @@ -27,7 +28,8 @@ class DropBox { try { if (data != "") { - val postData: ByteArray = data.toByteArray(StandardCharsets.UTF_8) + // StandardCharsets.UTF_8 requires API 19 + val postData: ByteArray = data.toByteArray(Charset.forName("UTF-8")) val outputStream = DataOutputStream(outputStream) outputStream.write(postData) outputStream.flush() @@ -103,4 +105,14 @@ class OnlineMultiplayer { val zippedGameInfo = DropBox().downloadFileAsString(getGameLocation(gameId)) return GameSaver().gameInfoFromString(Gzip.unzip(zippedGameInfo)) } + + /** + * Returns current turn's player. + * Does not initialize transitive GameInfo data. + * It is therefore stateless and save to call for Multiplayer Turn Notifier, unlike tryDownloadGame(). + */ + fun tryDownloadCurrentTurnCiv(gameId: String): CivilizationInfo { + val zippedGameInfo = DropBox().downloadFileAsString(getGameLocation(gameId)) + return GameSaver().currentTurnCivFromString(Gzip.unzip(zippedGameInfo)) + } }