diff --git a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt index 99d2dc15c..0a4a84e59 100644 --- a/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt +++ b/synchronization/src/androidTest/kotlin/de/cyface/synchronization/SyncPerformerTest.kt @@ -75,7 +75,7 @@ import java.net.URL * * @author Klemens Muthmann * @author Armin Schnabel - * @version 1.0.0 + * @version 1.0.1 * @since 7.7.0 */ @RunWith(AndroidJUnit4::class) @@ -92,7 +92,7 @@ class SyncPerformerTest { persistence = DefaultPersistenceLayer(context, DefaultPersistenceBehaviour()) SharedTestUtils.clearPersistenceLayer(context, persistence) - oocut = SyncPerformer(context) + oocut = SyncPerformer(context, true) } @After diff --git a/synchronization/src/cyface/kotlin/de/cyface/synchronization/CyfaceAuthenticator.kt b/synchronization/src/cyface/kotlin/de/cyface/synchronization/CyfaceAuthenticator.kt index b6e6d4f50..fd45d81c9 100644 --- a/synchronization/src/cyface/kotlin/de/cyface/synchronization/CyfaceAuthenticator.kt +++ b/synchronization/src/cyface/kotlin/de/cyface/synchronization/CyfaceAuthenticator.kt @@ -44,7 +44,7 @@ import kotlinx.coroutines.runBlocking * * @author Klemens Muthmann * @author Armin Schnabel - * @version 5.1.0 + * @version 5.1.1 * @since 2.0.0 */ class CyfaceAuthenticator(private val context: Context) : @@ -112,7 +112,8 @@ class CyfaceAuthenticator(private val context: Context) : ErrorHandler.sendErrorIntent( context, ErrorHandler.ErrorCode.UNKNOWN.code, - e.message + e.message, + false // login currently only happens while the user is active ) throw NetworkErrorException(e) } diff --git a/synchronization/src/main/java/de/cyface/synchronization/ErrorHandler.java b/synchronization/src/main/java/de/cyface/synchronization/ErrorHandler.java index a89a7495b..dc993da82 100644 --- a/synchronization/src/main/java/de/cyface/synchronization/ErrorHandler.java +++ b/synchronization/src/main/java/de/cyface/synchronization/ErrorHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 Cyface GmbH + * Copyright 2018-2023 Cyface GmbH * * This file is part of the Cyface SDK for Android. * @@ -41,13 +41,14 @@ * support time for all involved. * * @author Armin Schnabel - * @version 2.1.0 + * @version 2.2.0 * @since 2.2.0 */ public class ErrorHandler extends BroadcastReceiver { public final static String ERROR_INTENT = "de.cyface.error"; public final static String ERROR_CODE_EXTRA = "de.cyface.error.error_code"; + public final static String ERROR_BACKGROUND_EXTRA = "de.cyface.error.from_background"; public final static String HTTP_CODE_EXTRA = "de.cyface.error.http_code"; private final Collection errorListeners; @@ -95,11 +96,15 @@ public static void sendErrorIntent(final Context context, final int errorCode, f * * @param context the {@link Context} * @param errorCode the Cyface error code + * @param message A message which can be shown to the user, e.g. as toast. + * @param fromBackground `true` if the error was caused without user interaction, e.g. to avoid + * disturbing the user while he is not using the app. */ - public static void sendErrorIntent(final Context context, final int errorCode, @Nullable final String message) { + public static void sendErrorIntent(final Context context, final int errorCode, @Nullable final String message, final boolean fromBackground) { final Intent intent = new Intent(ERROR_INTENT); intent.putExtra(ERROR_CODE_EXTRA, errorCode); + intent.putExtra(ERROR_BACKGROUND_EXTRA, fromBackground); LocalBroadcastManager.getInstance(context).sendBroadcast(intent); if (message != null) { Log.d(TAG, message); @@ -111,6 +116,7 @@ public void onReceive(final Context context, final Intent intent) { Validate.notNull(intent.getExtras()); final int errorCodeInt = intent.getExtras().getInt(ERROR_CODE_EXTRA); + final var fromBackground = intent.getExtras().getBoolean(ERROR_BACKGROUND_EXTRA); final ErrorCode errorCode = ErrorCode.getValueForCode(errorCodeInt); Validate.notNull(errorCode); String errorMessage; @@ -220,7 +226,7 @@ public void onReceive(final Context context, final Intent intent) { } for (final ErrorListener errorListener : errorListeners) { - errorListener.onErrorReceive(errorCode, errorMessage); + errorListener.onErrorReceive(errorCode, errorMessage, fromBackground); } } @@ -274,10 +280,15 @@ public static ErrorCode getValueForCode(final int code) { * @since 1.0.0 */ public interface ErrorListener { - // These enhanced error details will be (re)implemented with #CY-3709 - // @ param causeId Optional id of the cause object for the error, e.g. measurementId - // @ param causeExtra Optional, additional information such as the date of a broken measurement - void onErrorReceive(final ErrorCode errorCode, final String errorMessage); + /** + * Handler called upon new errors. + * + * @param errorCode the Cyface error code + * @param errorMessage A message which can be shown to the user, e.g. as toast. + * @param fromBackground `true` if the error was caused without user interaction, e.g. to avoid + * disturbing the user while he is not using the app. + */ + void onErrorReceive(final ErrorCode errorCode, final String errorMessage, final boolean fromBackground); } // The following error handling will be (re)implemented with #CY-3709 diff --git a/synchronization/src/main/kotlin/de/cyface/synchronization/SyncAdapter.kt b/synchronization/src/main/kotlin/de/cyface/synchronization/SyncAdapter.kt index fd680a2a3..a7cfd4b89 100644 --- a/synchronization/src/main/kotlin/de/cyface/synchronization/SyncAdapter.kt +++ b/synchronization/src/main/kotlin/de/cyface/synchronization/SyncAdapter.kt @@ -24,6 +24,7 @@ import android.accounts.NetworkErrorException import android.content.AbstractThreadedSyncAdapter import android.content.ContentProviderClient import android.content.ContentResolver +import android.content.ContentResolver.SYNC_EXTRAS_MANUAL import android.content.Context import android.content.SyncResult import android.content.pm.PackageManager @@ -56,7 +57,7 @@ import java.io.File * * @author Armin Schnabel * @author Klemens Muthmann - * @version 4.1.0 + * @version 4.1.1 * @since 2.0.0 * @property authenticator The authenticator to use for synchronization. * @property uploader The uploader to use for synchronization. @@ -122,7 +123,10 @@ class SyncAdapter private constructor( context, DefaultPersistenceBehaviour() ) - val syncPerformer = SyncPerformer(context) + // Ensure sync errors are shown to the user when triggering sync manually + val fromBackground = !extras.getBoolean(SYNC_EXTRAS_MANUAL) + val syncPerformer = SyncPerformer(context, fromBackground) + // Ensure user is authorized before starting synchronization authenticator.performActionWithFreshTokens { _, _, ex -> @@ -132,7 +136,8 @@ class SyncAdapter private constructor( ErrorHandler.sendErrorIntent( context, ErrorCode.AUTHENTICATION_ERROR.code, - ex.message + ex.message, + fromBackground ) } else { try { @@ -211,7 +216,8 @@ class SyncAdapter private constructor( ErrorHandler.sendErrorIntent( context, ErrorCode.AUTHENTICATION_ERROR.code, - e.message + e.message, + fromBackground ) } else { val result = syncPerformer.sendData( @@ -272,14 +278,20 @@ class SyncAdapter private constructor( } catch (e: CursorIsNullException) { Log.w(TAG, e.javaClass.simpleName + ": " + e.message) syncResult.databaseError = true - ErrorHandler.sendErrorIntent(context, ErrorCode.DATABASE_ERROR.code, e.message) + ErrorHandler.sendErrorIntent( + context, + ErrorCode.DATABASE_ERROR.code, + e.message, + fromBackground + ) } catch (e: AuthenticatorException) { Log.w(TAG, e.javaClass.simpleName + ": " + e.message) syncResult.stats.numAuthExceptions++ ErrorHandler.sendErrorIntent( context, ErrorCode.AUTHENTICATION_ERROR.code, - e.message + e.message, + fromBackground ) } catch (e: SynchronizationInterruptedException) { Log.w(TAG, e.javaClass.simpleName + ": " + e.message) @@ -287,7 +299,8 @@ class SyncAdapter private constructor( ErrorHandler.sendErrorIntent( context, ErrorCode.SYNCHRONIZATION_INTERRUPTED.code, - e.message + e.message, + fromBackground ) } catch (e: NetworkErrorException) { Log.w(TAG, e.javaClass.simpleName + ": " + e.message) diff --git a/synchronization/src/main/kotlin/de/cyface/synchronization/SyncPerformer.kt b/synchronization/src/main/kotlin/de/cyface/synchronization/SyncPerformer.kt index f4876b5f2..52d1582a0 100644 --- a/synchronization/src/main/kotlin/de/cyface/synchronization/SyncPerformer.kt +++ b/synchronization/src/main/kotlin/de/cyface/synchronization/SyncPerformer.kt @@ -56,11 +56,13 @@ import java.net.MalformedURLException * * @author Klemens Muthmann * @author Armin Schnabel - * @version 7.0.0 + * @version 8.0.0 * @since 2.0.0 * @property context The Android `Context` to use for setting the correct server certification information. + * @property fromBackground `true` if the error was caused without user interaction, e.g. to avoid + * disturbing the user while he is not using the app. */ -internal class SyncPerformer(private val context: Context) { +internal class SyncPerformer(private val context: Context, private val fromBackground: Boolean) { /** * Triggers the data transmission to a Cyface server API. The `measurementIdentifier` and * `deviceIdentifier` need to be globally unique. If they are not the server will probably reject the @@ -135,7 +137,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.SERVER_UNAVAILABLE.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -145,7 +148,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.FORBIDDEN.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -155,7 +159,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.MALFORMED_URL.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -165,7 +170,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.SYNCHRONIZATION_ERROR.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -175,7 +181,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.UNAUTHORIZED.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -185,7 +192,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.INTERNAL_SERVER_ERROR.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -195,7 +203,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.ENTITY_NOT_PARSABLE.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -205,7 +214,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.BAD_REQUEST.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -215,7 +225,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.NETWORK_UNAVAILABLE.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -225,7 +236,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.SYNCHRONIZATION_INTERRUPTED.code, - e.message + e.message, + fromBackground ) e.printStackTrace() Result.UPLOAD_FAILED @@ -236,7 +248,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.TOO_MANY_REQUESTS.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -246,7 +259,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.UPLOAD_SESSION_EXPIRED.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -256,7 +270,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.UNEXPECTED_RESPONSE_CODE.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED } @@ -266,7 +281,8 @@ internal class SyncPerformer(private val context: Context) { ErrorHandler.sendErrorIntent( context, ErrorCode.ACCOUNT_NOT_ACTIVATED.code, - e.message + e.message, + fromBackground ) Result.UPLOAD_FAILED }