Fixed Multiplayer Turn Notifier periodically failing with error notification. (#2054)

This commit is contained in:
wrov 2020-03-02 05:44:53 +01:00 committed by GitHub
parent 10b95c6c1c
commit 1df0c408aa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 96 additions and 29 deletions

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.unciv.app" > package="com.unciv.app" >
<uses-sdk/> <uses-sdk/>
@ -17,13 +18,14 @@
android:launchMode="singleTask" android:launchMode="singleTask"
android:label="@string/app_name" android:label="@string/app_name"
android:screenOrientation="userLandscape" android:screenOrientation="userLandscape"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"> android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
tools:ignore="LockedOrientationActivity">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<receiver android:name=".CopyToClipboardReceiver" android:exported="false" />
</application> </application>
</manifest> </manifest>

View file

@ -4,8 +4,8 @@
<string name="Notify_YourTurn_Short">Unciv - Du bist am Zug!</string> <string name="Notify_YourTurn_Short">Unciv - Du bist am Zug!</string>
<string name="Notify_YourTurn_Long">Deine Freunde warten auf deinen Zug.</string> <string name="Notify_YourTurn_Long">Deine Freunde warten auf deinen Zug.</string>
<string name="Notify_Error_Short">Ein Fehler ist aufgetreten</string> <string name="Notify_Error_Short">Ein Fehler ist aufgetreten</string>
<string name="Notify_Error_Long">Multiplayer Zug Benachrichtigungsdienst wurde beendet</string> <string name="Notify_Error_Long">Multiplayer Zug Benachrichtigungsdienst wurde beendet.</string>
<string name="Notify_Persist_Short">Letzter online Zug Check:</string> <string name="Notify_Persist_Short">Letzter online Zugcheck:</string>
<string name="Notify_Persist_Long_P1">Unciv wird dich benachrichtigen, wenn du im Multiplayer am Zug bist.</string> <string name="Notify_Persist_Long_P1">Unciv wird dich benachrichtigen, wenn du im Multiplayer am Zug bist.</string>
<string name="Notify_Persist_Long_P2">Prüft etwa alle</string> <string name="Notify_Persist_Long_P2">Prüft etwa alle</string>
<string name="Notify_Persist_Long_P3">Minute(n) wenn Internet vorhanden.</string> <string name="Notify_Persist_Long_P3">Minute(n) wenn Internet vorhanden.</string>
@ -14,4 +14,7 @@
<string name="Notify_ChannelInfo_Long">Informiert dich, wenn du im Multiplayer am Zug bist.</string> <string name="Notify_ChannelInfo_Long">Informiert dich, wenn du im Multiplayer am Zug bist.</string>
<string name="Notify_ChannelService_Short">Unciv Multiplayer Zugprüfer Persistenter Status</string> <string name="Notify_ChannelService_Short">Unciv Multiplayer Zugprüfer Persistenter Status</string>
<string name="Notify_ChannelService_Long">Permanent angezeigte Benachrichtigung, welche dich über die Hintergrundaktivität des Dienstes informiert.</string> <string name="Notify_ChannelService_Long">Permanent angezeigte Benachrichtigung, welche dich über die Hintergrundaktivität des Dienstes informiert.</string>
<string name="Notify_Error_StackTrace_Toast">Stacktraces in Zwischenablage kopiert.</string>
<string name="Notify_Error_CopyAction">Kopiere Stacktraces in Zwischenablage</string>
<string name="Notify_Error_Retrying">Fehler, wiederhole…</string>
</resources> </resources>

View file

@ -4,7 +4,7 @@
<string name="Notify_YourTurn_Short">Unciv - It\'s your turn!</string> <string name="Notify_YourTurn_Short">Unciv - It\'s your turn!</string>
<string name="Notify_YourTurn_Long">Your friends are waiting on your turn.</string> <string name="Notify_YourTurn_Long">Your friends are waiting on your turn.</string>
<string name="Notify_Error_Short">An error has occurred</string> <string name="Notify_Error_Short">An error has occurred</string>
<string name="Notify_Error_Long">Multiplayer turn notifier service terminated</string> <string name="Notify_Error_Long">Multiplayer turn notifier service terminated.</string>
<string name="Notify_Persist_Short">Last online turn check:</string> <string name="Notify_Persist_Short">Last online turn check:</string>
<string name="Notify_Persist_Long_P1">Unciv will inform you when it\'s your turn in multiplayer.</string> <string name="Notify_Persist_Long_P1">Unciv will inform you when it\'s your turn in multiplayer.</string>
<string name="Notify_Persist_Long_P2">Checks ca. every</string> <string name="Notify_Persist_Long_P2">Checks ca. every</string>
@ -14,4 +14,7 @@
<string name="Notify_ChannelInfo_Long">Informs you when it\'s your turn in multiplayer.</string> <string name="Notify_ChannelInfo_Long">Informs you when it\'s your turn in multiplayer.</string>
<string name="Notify_ChannelService_Short">Unciv Multiplayer Turn Checker Persistent Status</string> <string name="Notify_ChannelService_Short">Unciv Multiplayer Turn Checker Persistent Status</string>
<string name="Notify_ChannelService_Long">Shown constantly to inform you about background checking.</string> <string name="Notify_ChannelService_Long">Shown constantly to inform you about background checking.</string>
<string name="Notify_Error_StackTrace_Toast">Stacktraces copied to clipboard.</string>
<string name="Notify_Error_CopyAction">Copy stacktrace to clipboard</string>
<string name="Notify_Error_Retrying">failed, retrying…</string>
</resources> </resources>

View file

@ -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()
}
}

View file

@ -15,6 +15,9 @@ import com.badlogic.gdx.backends.android.AndroidApplication
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.ui.worldscreen.mainmenu.OnlineMultiplayer 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.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -24,6 +27,7 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
companion object { companion object {
const val WORK_TAG = "UNCIV_MULTIPLAYER_TURN_CHECKER_WORKER" 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_SERVICE = 1
const val NOTIFICATION_ID_INFO = 2 const val NOTIFICATION_ID_INFO = 2
@ -202,41 +206,49 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
} }
override fun doWork(): Result { override fun doWork(): Result {
val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)
val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5)
try { try {
val latestGame = OnlineMultiplayer().tryDownloadGame(inputData.getString(GAME_ID)!!) val currentTurnPlayer = OnlineMultiplayer().tryDownloadCurrentTurnCiv(inputData.getString(GAME_ID)!!)
if (latestGame.currentPlayerCiv.playerId == inputData.getString(USER_ID)!!) { if (currentTurnPlayer.playerId == inputData.getString(USER_ID)!!) {
notifyUserAboutTurn(applicationContext) notifyUserAboutTurn(applicationContext)
with(NotificationManagerCompat.from(applicationContext)) { with(NotificationManagerCompat.from(applicationContext)) {
cancel(NOTIFICATION_ID_SERVICE) cancel(NOTIFICATION_ID_SERVICE)
} }
} else { } else {
updatePersistentNotification(inputData) if (showPersistNotific) { updatePersistentNotification(inputData) }
// We have to reset the fail counter since no exception appeared // We have to reset the fail counter since no exception appeared
val inputDataFailReset = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, 0).build() 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) { } catch (ex: Exception) {
val failCount = inputData.getInt(FAIL_COUNT, 0) val failCount = inputData.getInt(FAIL_COUNT, 0)
if (failCount > 3) { if (failCount > 3) {
showErrorNotification() showErrorNotification(getStackTraceString(ex))
with(NotificationManagerCompat.from(applicationContext)) { with(NotificationManagerCompat.from(applicationContext)) {
cancel(NOTIFICATION_ID_SERVICE) cancel(NOTIFICATION_ID_SERVICE)
} }
return Result.failure() return Result.failure()
} else { } else {
if (showPersistNotific) { showPersistentNotification(applicationContext,
applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) }
// If check fails, retry in one minute. // If check fails, retry in one minute.
// Makes sense, since checks only happen if Internet is available in principle. // 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. // 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() val inputDataFailIncrease = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, failCount + 1).build()
enqueue(applicationContext, 1, inputDataFailIncrease) enqueue(applicationContext, 1, inputDataFailIncrease)
// Persistent Notification is not updated, because user may think check succeed.
} }
} }
return Result.success() return Result.success()
} }
private fun getStackTraceString(ex: Exception): String {
val writer: Writer = StringWriter()
ex.printStackTrace(PrintWriter(writer))
return writer.toString()
}
private fun updatePersistentNotification(inputData: Data) { private fun updatePersistentNotification(inputData: Data) {
if (inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)) {
val cal = GregorianCalendar.getInstance() val cal = GregorianCalendar.getInstance()
val hour = cal.get(GregorianCalendar.HOUR_OF_DAY).toString() val hour = cal.get(GregorianCalendar.HOUR_OF_DAY).toString()
var minute = cal.get(GregorianCalendar.MINUTE).toString() var minute = cal.get(GregorianCalendar.MINUTE).toString()
@ -248,14 +260,18 @@ class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParame
showPersistentNotification(applicationContext, displayTime, showPersistentNotification(applicationContext, displayTime,
inputData.getInt(CONFIGURED_DELAY, 5).toString()) inputData.getInt(CONFIGURED_DELAY, 5).toString())
} }
}
private fun showErrorNotification() { private fun showErrorNotification(stackTraceString: String) {
val pendingIntent: PendingIntent = val pendingLaunchGameIntent: PendingIntent =
Intent(applicationContext, AndroidLauncher::class.java).let { notificationIntent -> Intent(applicationContext, AndroidLauncher::class.java).let { notificationIntent ->
PendingIntent.getActivity(applicationContext, 0, notificationIntent, 0) 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) val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO)
.setPriority(NotificationManagerCompat.IMPORTANCE_DEFAULT) // No direct user action expected .setPriority(NotificationManagerCompat.IMPORTANCE_DEFAULT) // No direct user action expected
.setContentTitle(applicationContext.resources.getString(R.string.Notify_Error_Short)) .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 // without at least vibrate, some Android versions don't show a heads-up notification
.setDefaults(DEFAULT_VIBRATE) .setDefaults(DEFAULT_VIBRATE)
.setLights(Color.YELLOW, 300, 100) .setLights(Color.YELLOW, 300, 100)
.setContentIntent(pendingIntent) .setContentIntent(pendingLaunchGameIntent)
.setCategory(NotificationCompat.CATEGORY_ERROR) .setCategory(NotificationCompat.CATEGORY_ERROR)
.setOngoing(false) .setOngoing(false)
.addAction(0, applicationContext.resources.getString(R.string.Notify_Error_CopyAction), pendingCopyClipboardIntent)
with(NotificationManagerCompat.from(applicationContext)) { with(NotificationManagerCompat.from(applicationContext)) {
notify(NOTIFICATION_ID_INFO, notification.build()) notify(NOTIFICATION_ID_INFO, notification.build())

View file

@ -3,6 +3,7 @@ package com.unciv.logic
import com.badlogic.gdx.Gdx import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.utils.Json import com.badlogic.gdx.utils.Json
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.models.metadata.GameSettings import com.unciv.models.metadata.GameSettings
import com.unciv.ui.utils.ImageGetter import com.unciv.ui.utils.ImageGetter
import java.io.File 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)
}
} }

View file

@ -2,6 +2,7 @@ package com.unciv.ui.worldscreen.mainmenu
import com.unciv.logic.GameInfo import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver import com.unciv.logic.GameSaver
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.ui.saves.Gzip import com.unciv.ui.saves.Gzip
import java.io.BufferedReader import java.io.BufferedReader
import java.io.DataOutputStream import java.io.DataOutputStream
@ -9,7 +10,7 @@ import java.io.InputStream
import java.io.InputStreamReader import java.io.InputStreamReader
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.nio.charset.StandardCharsets import java.nio.charset.Charset
class DropBox { class DropBox {
@ -27,7 +28,8 @@ class DropBox {
try { try {
if (data != "") { 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) val outputStream = DataOutputStream(outputStream)
outputStream.write(postData) outputStream.write(postData)
outputStream.flush() outputStream.flush()
@ -103,4 +105,14 @@ class OnlineMultiplayer {
val zippedGameInfo = DropBox().downloadFileAsString(getGameLocation(gameId)) val zippedGameInfo = DropBox().downloadFileAsString(getGameLocation(gameId))
return GameSaver().gameInfoFromString(Gzip.unzip(zippedGameInfo)) 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))
}
} }