Skip to main content

Error Handling Guide

This guide covers comprehensive error handling strategies for the Cirvia Parental SDK. Learn how to build resilient applications that gracefully handle monitoring failures without breaking core functionality.

Overview​

Robust error handling ensures that:

  • App functionality never breaks due to monitoring issues
  • Users have smooth experiences even when monitoring fails
  • Monitoring resumes automatically when conditions improve
  • Developers get actionable error information for debugging

Error Categories​

public class NetworkErrorHandler {

public void handleNetworkError(Exception error, String content, String platform) {
if (error instanceof UnknownHostException) {
// No internet connection
Log.w("Monitor", "No internet connection - queuing for retry");
queueForRetry(content, platform);

} else if (error instanceof SocketTimeoutException) {
// Request timeout
Log.w("Monitor", "Request timeout - will retry with backoff");
scheduleRetryWithBackoff(content, platform, 1);

} else if (error instanceof ConnectException) {
// Server unreachable
Log.w("Monitor", "Server unreachable - checking service status");
checkServiceStatusAndRetry(content, platform);

} else {
// Other network issues
Log.e("Monitor", "Network error: " + error.getMessage());
handleGenericNetworkError(content, platform);
}
}

private void queueForRetry(String content, String platform) {
// Queue content for monitoring when network returns
OfflineQueue.getInstance().addItem(content, platform);

// Register network callback to process queue when online
registerNetworkCallback();
}

private void scheduleRetryWithBackoff(String content, String platform, int attempt) {
if (attempt > 5) {
Log.e("Monitor", "Max retry attempts reached");
return;
}

long delayMs = Math.min(1000 * (long) Math.pow(2, attempt), 30000); // Max 30 seconds

new Handler(Looper.getMainLooper()).postDelayed(() -> {
try {
ParentalAI.sendTextIncident(platform, content);
} catch (Exception e) {
handleNetworkError(e, content, platform);
}
}, delayMs);
}
}

Authentication Errors​

public class AuthErrorHandler {

public void handleAuthError(Exception error, Runnable onResolved) {
if (error instanceof IllegalStateException) {
String message = error.getMessage();

if (message != null && message.contains("not initialized")) {
Log.w("Auth", "SDK not initialized - attempting re-initialization");
attemptReinitialization(onResolved);

} else if (message.contains("Google account")) {
Log.e("Auth", "Google account issue - user intervention required");
showGoogleAccountError();

} else {
Log.e("Auth", "Unknown authentication state error");
handleGenericAuthError(onResolved);
}
}
}

private void attemptReinitialization(Runnable onResolved) {
ParentalAIConfig config = ConfigManager.getCurrentConfig();

ParentalAI.init(getApplicationContext(), config,
() -> {
Log.i("Auth", "Re-initialization successful");
if (onResolved != null) onResolved.run();
},
() -> {
Log.e("Auth", "Re-initialization failed");
showAuthFailureDialog();
}
);
}

private void showGoogleAccountError() {
// Show user-friendly dialog explaining Google account requirement
new AlertDialog.Builder(getCurrentActivity())
.setTitle("Account Required")
.setMessage("Parental monitoring requires a Google account. Please sign in to continue.")
.setPositiveButton("Sign In", (dialog, which) -> {
// Launch Google sign-in flow
initiateGoogleSignIn();
})
.setNegativeButton("Skip", (dialog, which) -> {
// Continue without monitoring
proceedWithoutMonitoring();
})
.show();
}
}

Content Processing Errors​

public class ContentErrorHandler {

public void handleContentError(Exception error, String content, String platform) {
if (error instanceof IllegalArgumentException) {
String message = error.getMessage();

if (message != null && message.contains("null")) {
Log.w("Content", "Null content provided - skipping monitoring");
return;

} else if (message.contains("empty")) {
Log.w("Content", "Empty content provided - skipping monitoring");
return;

} else {
Log.e("Content", "Content validation error: " + message);
sanitizeAndRetry(content, platform);
}

} else if (error instanceof OutOfMemoryError) {
Log.e("Content", "Out of memory processing content");
handleMemoryError(content, platform);

} else {
Log.e("Content", "Unexpected content processing error", error);
handleGenericContentError(content, platform);
}
}

private void sanitizeAndRetry(String content, String platform) {
try {
// Clean and validate content
String sanitized = ContentSanitizer.sanitize(content);

if (ContentValidator.isValid(sanitized)) {
ParentalAI.sendTextIncident(platform, sanitized);
} else {
Log.w("Content", "Content failed validation after sanitization");
}

} catch (Exception e) {
Log.e("Content", "Failed to sanitize content", e);
}
}

private void handleMemoryError(String content, String platform) {
// Try to free memory
System.gc();

// Truncate content if too large
if (content.length() > 1000) {
String truncated = content.substring(0, 1000) + "...";
try {
ParentalAI.sendTextIncident(platform, truncated);
} catch (Exception e) {
Log.e("Content", "Failed to monitor truncated content", e);
}
}
}
}

Comprehensive Error Handling Framework​

Centralized Error Manager​

public class CirviaErrorManager {
private static final String TAG = "CirviaError";
private final Context context;
private final ErrorReporter errorReporter;
private final RetryManager retryManager;

public CirviaErrorManager(Context context) {
this.context = context;
this.errorReporter = new ErrorReporter();
this.retryManager = new RetryManager();
}

public void handleError(CirviaError error) {
// Log error for debugging
logError(error);

// Report to analytics (if configured)
errorReporter.reportError(error);

// Attempt recovery based on error type
switch (error.getType()) {
case NETWORK:
handleNetworkError(error);
break;
case AUTHENTICATION:
handleAuthenticationError(error);
break;
case CONTENT_PROCESSING:
handleContentError(error);
break;
case API_RATE_LIMIT:
handleRateLimitError(error);
break;
case SERVER_ERROR:
handleServerError(error);
break;
default:
handleUnknownError(error);
break;
}
}

private void logError(CirviaError error) {
String logMessage = String.format(
"Cirvia Error [%s]: %s (Code: %d)",
error.getType(),
error.getMessage(),
error.getErrorCode()
);

if (error.getSeverity() == ErrorSeverity.HIGH) {
Log.e(TAG, logMessage, error.getCause());
} else {
Log.w(TAG, logMessage);
}
}

private void handleNetworkError(CirviaError error) {
if (isNetworkAvailable()) {
// Network available but request failed - server issue
retryManager.scheduleRetry(error.getRetryableAction(), RetryStrategy.EXPONENTIAL_BACKOFF);
} else {
// No network - queue for later
OfflineQueue.getInstance().addError(error);
registerConnectivityListener();
}
}

private void handleRateLimitError(CirviaError error) {
// Respect rate limits - wait before retrying
long waitTime = extractWaitTimeFromError(error);
retryManager.scheduleRetry(error.getRetryableAction(), waitTime);

// Reduce monitoring frequency temporarily
MonitoringThrottler.getInstance().enableThrottling(waitTime);
}
}

Custom Error Classes​

public class CirviaError {
public enum Type {
NETWORK, AUTHENTICATION, CONTENT_PROCESSING,
API_RATE_LIMIT, SERVER_ERROR, CONFIGURATION, UNKNOWN
}

public enum Severity {
LOW, MEDIUM, HIGH, CRITICAL
}

private final Type type;
private final Severity severity;
private final String message;
private final int errorCode;
private final Throwable cause;
private final RetryableAction retryableAction;
private final long timestamp;

public CirviaError(Type type, Severity severity, String message,
int errorCode, Throwable cause, RetryableAction retryableAction) {
this.type = type;
this.severity = severity;
this.message = message;
this.errorCode = errorCode;
this.cause = cause;
this.retryableAction = retryableAction;
this.timestamp = System.currentTimeMillis();
}

// Getters...
public Type getType() { return type; }
public Severity getSeverity() { return severity; }
public String getMessage() { return message; }
public int getErrorCode() { return errorCode; }
public Throwable getCause() { return cause; }
public RetryableAction getRetryableAction() { return retryableAction; }
public long getTimestamp() { return timestamp; }

// Builder pattern for easy creation
public static class Builder {
private Type type = Type.UNKNOWN;
private Severity severity = Severity.MEDIUM;
private String message = "Unknown error";
private int errorCode = -1;
private Throwable cause;
private RetryableAction retryableAction;

public Builder type(Type type) { this.type = type; return this; }
public Builder severity(Severity severity) { this.severity = severity; return this; }
public Builder message(String message) { this.message = message; return this; }
public Builder errorCode(int errorCode) { this.errorCode = errorCode; return this; }
public Builder cause(Throwable cause) { this.cause = cause; return this; }
public Builder retryableAction(RetryableAction action) { this.retryableAction = action; return this; }

public CirviaError build() {
return new CirviaError(type, severity, message, errorCode, cause, retryableAction);
}
}
}

@FunctionalInterface
public interface RetryableAction {
void execute() throws Exception;
}

Robust Monitoring Wrapper​

public class RobustMonitoringWrapper {
private final CirviaErrorManager errorManager;
private final MonitoringConfig config;

public RobustMonitoringWrapper(Context context) {
this.errorManager = new CirviaErrorManager(context);
this.config = new MonitoringConfig();
}

public void monitorTextSafely(String platform, String content) {
monitorTextSafely(platform, content, null);
}

public void monitorTextSafely(String platform, String content, Runnable onSuccess) {
try {
// Pre-validation
if (!validateInputs(platform, content)) {
return;
}

// Attempt monitoring
ParentalAI.sendTextIncident(platform, content);

// Success callback
if (onSuccess != null) {
onSuccess.run();
}

} catch (IllegalStateException e) {
handleAuthError(e, platform, content);

} catch (IllegalArgumentException e) {
handleValidationError(e, platform, content);

} catch (Exception e) {
handleUnexpectedError(e, platform, content);
}
}

public void monitorImageSafely(String platform, Bitmap image) {
monitorImageSafely(platform, image, null);
}

public void monitorImageSafely(String platform, Bitmap image, Runnable onSuccess) {
// Process on background thread to avoid blocking UI
new Thread(() -> {
try {
// Pre-validation
if (!validateImageInputs(platform, image)) {
return;
}

// Encode image safely
String base64Image = safeImageEncoding(image);
if (base64Image == null) {
return;
}

// Attempt monitoring
ParentalAI.sendImageIncident(platform, base64Image);

// Success callback on main thread
if (onSuccess != null) {
new Handler(Looper.getMainLooper()).post(onSuccess);
}

} catch (OutOfMemoryError e) {
handleMemoryError(e, platform, image);

} catch (Exception e) {
handleImageError(e, platform, image);
}
}).start();
}

private boolean validateInputs(String platform, String content) {
if (platform == null || platform.trim().isEmpty()) {
Log.w("Monitor", "Invalid platform identifier");
return false;
}

if (content == null || content.trim().isEmpty()) {
Log.d("Monitor", "Empty content - skipping monitoring");
return false;
}

if (content.length() > config.getMaxContentLength()) {
Log.w("Monitor", "Content too long - truncating");
// Could truncate here if desired
}

return true;
}

private String safeImageEncoding(Bitmap image) {
try {
return ImageEncoder.encodeImageToBase64(image);
} catch (OutOfMemoryError e) {
Log.w("Monitor", "OOM during encoding - trying smaller size");
try {
// Try with smaller image
Bitmap smallerImage = Bitmap.createScaledBitmap(image,
image.getWidth() / 2, image.getHeight() / 2, true);
String result = ImageEncoder.encodeImageToBase64(smallerImage);
smallerImage.recycle();
return result;
} catch (Exception e2) {
Log.e("Monitor", "Failed to encode even smaller image", e2);
return null;
}
} catch (Exception e) {
Log.e("Monitor", "Unexpected error encoding image", e);
return null;
}
}

private void handleAuthError(IllegalStateException e, String platform, String content) {
CirviaError error = new CirviaError.Builder()
.type(CirviaError.Type.AUTHENTICATION)
.severity(CirviaError.Severity.HIGH)
.message("SDK authentication failed")
.cause(e)
.retryableAction(() -> ParentalAI.sendTextIncident(platform, content))
.build();

errorManager.handleError(error);
}
}

Retry Mechanisms​

Intelligent Retry Manager​

public class RetryManager {
private final ScheduledExecutorService scheduler;
private final Map<String, RetryState> retryStates;

public RetryManager() {
scheduler = Executors.newScheduledThreadPool(2);
retryStates = new ConcurrentHashMap<>();
}

public void scheduleRetry(RetryableAction action, RetryStrategy strategy) {
String actionId = generateActionId(action);
RetryState state = retryStates.getOrDefault(actionId, new RetryState());

if (state.getAttemptCount() >= strategy.getMaxAttempts()) {
Log.e("Retry", "Max retry attempts reached for action: " + actionId);
retryStates.remove(actionId);
return;
}

long delay = strategy.calculateDelay(state.getAttemptCount());
state.incrementAttempt();
retryStates.put(actionId, state);

scheduler.schedule(() -> {
try {
action.execute();
// Success - remove from retry tracking
retryStates.remove(actionId);

} catch (Exception e) {
Log.w("Retry", "Retry attempt failed: " + e.getMessage());
// Schedule another retry
scheduleRetry(action, strategy);
}
}, delay, TimeUnit.MILLISECONDS);
}

public void scheduleRetry(RetryableAction action, long delayMs) {
scheduler.schedule(() -> {
try {
action.execute();
} catch (Exception e) {
Log.w("Retry", "Scheduled retry failed: " + e.getMessage());
}
}, delayMs, TimeUnit.MILLISECONDS);
}
}

public enum RetryStrategy {
IMMEDIATE(1000, 3),
LINEAR_BACKOFF(2000, 5),
EXPONENTIAL_BACKOFF(1000, 7);

private final long baseDelay;
private final int maxAttempts;

RetryStrategy(long baseDelay, int maxAttempts) {
this.baseDelay = baseDelay;
this.maxAttempts = maxAttempts;
}

public long calculateDelay(int attemptNumber) {
switch (this) {
case IMMEDIATE:
return baseDelay;
case LINEAR_BACKOFF:
return baseDelay * attemptNumber;
case EXPONENTIAL_BACKOFF:
return Math.min(baseDelay * (long) Math.pow(2, attemptNumber), 30000);
default:
return baseDelay;
}
}

public int getMaxAttempts() { return maxAttempts; }
}

Offline Queue Management​

public class OfflineQueue {
private static OfflineQueue instance;
private final Queue<QueuedItem> queue;
private final SharedPreferences prefs;
private final Gson gson;

private OfflineQueue(Context context) {
queue = new ConcurrentLinkedQueue<>();
prefs = context.getSharedPreferences("cirvia_offline_queue", Context.MODE_PRIVATE);
gson = new Gson();
loadQueueFromStorage();
}

public static OfflineQueue getInstance(Context context) {
if (instance == null) {
instance = new OfflineQueue(context.getApplicationContext());
}
return instance;
}

public void addItem(String content, String platform) {
QueuedItem item = new QueuedItem(content, platform, System.currentTimeMillis());
queue.offer(item);
saveQueueToStorage();

// Limit queue size
while (queue.size() > 100) {
queue.poll();
}
}

public void processQueue() {
List<QueuedItem> processedItems = new ArrayList<>();

while (!queue.isEmpty()) {
QueuedItem item = queue.poll();

try {
if (item.isExpired()) {
Log.d("OfflineQueue", "Skipping expired item");
continue;
}

// Attempt to process item
ParentalAI.sendTextIncident(item.getPlatform(), item.getContent());
processedItems.add(item);

} catch (Exception e) {
// Put item back in queue if processing fails
queue.offer(item);
Log.w("OfflineQueue", "Failed to process queued item", e);
break; // Stop processing if current item fails
}
}

Log.i("OfflineQueue", "Processed " + processedItems.size() + " queued items");
saveQueueToStorage();
}

private void loadQueueFromStorage() {
String queueJson = prefs.getString("queue", null);
if (queueJson != null) {
try {
Type listType = new TypeToken<List<QueuedItem>>(){}.getType();
List<QueuedItem> items = gson.fromJson(queueJson, listType);
queue.addAll(items);
} catch (Exception e) {
Log.e("OfflineQueue", "Failed to load queue from storage", e);
}
}
}

private void saveQueueToStorage() {
try {
String queueJson = gson.toJson(new ArrayList<>(queue));
prefs.edit().putString("queue", queueJson).apply();
} catch (Exception e) {
Log.e("OfflineQueue", "Failed to save queue to storage", e);
}
}
}

class QueuedItem {
private final String content;
private final String platform;
private final long timestamp;
private static final long EXPIRY_TIME = 24 * 60 * 60 * 1000; // 24 hours

public QueuedItem(String content, String platform, long timestamp) {
this.content = content;
this.platform = platform;
this.timestamp = timestamp;
}

public boolean isExpired() {
return System.currentTimeMillis() - timestamp > EXPIRY_TIME;
}

// Getters...
public String getContent() { return content; }
public String getPlatform() { return platform; }
public long getTimestamp() { return timestamp; }
}

Network Connectivity Handling​

Connectivity Manager​

public class ConnectivityManager {
private final Context context;
private final ConnectivityManager.NetworkCallback networkCallback;
private boolean isRegistered = false;

public ConnectivityManager(Context context) {
this.context = context.getApplicationContext();
this.networkCallback = new ConnectivityManager.NetworkCallback() {
@Override
public void onAvailable(Network network) {
Log.i("Connectivity", "Network available - processing offline queue");
processOfflineQueue();
}

@Override
public void onLost(Network network) {
Log.w("Connectivity", "Network lost - monitoring will be queued");
}
};
}

public void registerNetworkCallback() {
if (!isRegistered) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
cm.registerDefaultNetworkCallback(networkCallback);
isRegistered = true;
}
}
}

public void unregisterNetworkCallback() {
if (isRegistered) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
cm.unregisterNetworkCallback(networkCallback);
isRegistered = false;
}
}
}

public boolean isNetworkAvailable() {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm != null) {
Network activeNetwork = cm.getActiveNetwork();
NetworkCapabilities caps = cm.getNetworkCapabilities(activeNetwork);
return caps != null && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
}
return false;
}

private void processOfflineQueue() {
new Thread(() -> {
try {
OfflineQueue.getInstance(context).processQueue();
} catch (Exception e) {
Log.e("Connectivity", "Error processing offline queue", e);
}
}).start();
}
}

User-Friendly Error Communication​

Error Dialog Manager​

public class ErrorDialogManager {

public static void showUserFriendlyError(Context context, CirviaError error) {
String title;
String message;
boolean showRetryOption = false;

switch (error.getType()) {
case NETWORK:
title = "Connection Issue";
message = "Unable to connect to monitoring service. Content will be monitored when connection is restored.";
break;

case AUTHENTICATION:
title = "Account Required";
message = "Parental monitoring requires authentication. Please sign in to continue.";
showRetryOption = true;
break;

case API_RATE_LIMIT:
title = "Service Busy";
message = "Monitoring service is currently busy. Your content is being queued for processing.";
break;

default:
title = "Monitoring Unavailable";
message = "Content monitoring is temporarily unavailable. Normal app functions continue to work.";
showRetryOption = true;
break;
}

AlertDialog.Builder builder = new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK", null);

if (showRetryOption) {
builder.setNeutralButton("Retry", (dialog, which) -> {
if (error.getRetryableAction() != null) {
try {
error.getRetryableAction().execute();
} catch (Exception e) {
Log.e("ErrorDialog", "Retry failed", e);
}
}
});
}

builder.show();
}

public static void showConfigurationError(Context context, String configIssue) {
new AlertDialog.Builder(context)
.setTitle("Configuration Error")
.setMessage("Monitoring configuration issue: " + configIssue +
"\n\nPlease contact app developer for assistance.")
.setPositiveButton("OK", null)
.show();
}
}

Testing Error Scenarios​

Error Simulation for Testing​

public class ErrorSimulator {

public static void simulateNetworkError() {
throw new ConnectException("Simulated network connection failure");
}

public static void simulateAuthError() {
throw new IllegalStateException("SDK not initialized");
}

public static void simulateMemoryError() {
throw new OutOfMemoryError("Simulated OOM error");
}

public static void simulateRateLimitError() {
// Simulate server response indicating rate limit
throw new RuntimeException("Rate limit exceeded - retry after 60 seconds");
}
}

@RunWith(AndroidJUnit4.class)
public class ErrorHandlingTest {

@Test
public void testNetworkErrorHandling() {
RobustMonitoringWrapper wrapper = new RobustMonitoringWrapper(getContext());

// Test with network error
try {
ErrorSimulator.simulateNetworkError();
} catch (Exception e) {
// Verify error is handled gracefully
wrapper.monitorTextSafely("test-platform", "test content");
}

// Verify app continues to function
assertTrue("App should continue functioning", true);
}

@Test
public void testRetryMechanism() {
RetryManager retryManager = new RetryManager();
AtomicInteger attemptCount = new AtomicInteger(0);

RetryableAction action = () -> {
attemptCount.incrementAndGet();
if (attemptCount.get() < 3) {
throw new RuntimeException("Simulated failure");
}
// Success on 3rd attempt
};

retryManager.scheduleRetry(action, RetryStrategy.EXPONENTIAL_BACKOFF);

// Wait for retries to complete
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

assertEquals("Should have attempted 3 times", 3, attemptCount.get());
}
}

Best Practices Summary​

  1. Never break app functionality - Handle all monitoring errors gracefully
  2. Implement retry logic - Use exponential backoff for transient failures
  3. Queue offline content - Process when connectivity returns
  4. Provide user feedback - Show helpful error messages when appropriate
  5. Log for debugging - Include sufficient detail for troubleshooting
  6. Test error scenarios - Simulate failures during development
  7. Monitor error rates - Track error frequency to identify issues
  8. Graceful degradation - Continue core app functions when monitoring fails

Integration Example​

Complete Error-Resilient Implementation​

public class ProductionReadyMonitor {
private final RobustMonitoringWrapper wrapper;
private final ConnectivityManager connectivityManager;
private final Context context;

public ProductionReadyMonitor(Context context) {
this.context = context.getApplicationContext();
this.wrapper = new RobustMonitoringWrapper(context);
this.connectivityManager = new ConnectivityManager(context);

// Register for network changes
connectivityManager.registerNetworkCallback();
}

public void monitorContent(String platform, String content) {
wrapper.monitorTextSafely(platform, content, () -> {
Log.d("Monitor", "Content monitored successfully");
});
}

public void monitorImage(String platform, Bitmap image) {
wrapper.monitorImageSafely(platform, image, () -> {
Log.d("Monitor", "Image monitored successfully");
});
}

public void cleanup() {
connectivityManager.unregisterNetworkCallback();
}
}

Next Steps​


Error handling complete! Your app will now gracefully handle monitoring failures while maintaining excellent user experience. Continue to Basic Integration Examples for complete implementation examples.