Skip to main content

Basic Integration Example

This example demonstrates a complete, production-ready integration of the Cirvia Parental SDK into an Android chat application. You'll see how to implement text and image monitoring with proper error handling.

Project Overview

We'll build a simple chat app called "SafeChat" that includes:

  • Real-time messaging with content monitoring
  • Image sharing with AI analysis
  • User authentication via Google OAuth
  • Robust error handling and offline support
  • Parent dashboard integration

Project Setup

1. Dependencies (build.gradle)

android {
compileSdk 34

defaultConfig {
applicationId "com.example.safechat"
minSdk 21
targetSdk 34
versionCode 1
versionName "1.0"
}

buildTypes {
debug {
buildConfigField "String", "CIRVIA_API_KEY", "\"${CIRVIA_DEV_API_KEY}\""
buildConfigField "String", "CIRVIA_INGEST_URL", "\"${CIRVIA_DEV_INGEST_URL}\""
buildConfigField "String", "CIRVIA_AUTH_URL", "\"${CIRVIA_DEV_AUTH_URL}\""
}
release {
buildConfigField "String", "CIRVIA_API_KEY", "\"${CIRVIA_PROD_API_KEY}\""
buildConfigField "String", "CIRVIA_INGEST_URL", "\"${CIRVIA_PROD_INGEST_URL}\""
buildConfigField "String", "CIRVIA_AUTH_URL", "\"${CIRVIA_PROD_AUTH_URL}\""
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
// Core Android dependencies
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.recyclerview:recyclerview:1.3.0'
implementation 'com.google.android.material:material:1.9.0'

// Cirvia Parental SDK dependencies
implementation 'com.squareup.okhttp3:okhttp:4.9.3'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.google.android.gms:play-services-auth:20.4.1'

// Image loading
implementation 'com.github.bumptech.glide:glide:4.14.2'

// Testing
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
}

2. Permissions (AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.safechat">

<!-- Required for Cirvia SDK -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- Image sharing -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".SafeChatApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>

Application Class

SafeChatApplication.java

package com.example.safechat;

import android.app.Application;
import android.util.Log;

import com.cirvia.parentalai.ParentalAI;
import com.cirvia.parentalai.config.ParentalAIConfig;

public class SafeChatApplication extends Application {
private static final String TAG = "SafeChatApp";
private boolean isCirviaReady = false;

@Override
public void onCreate() {
super.onCreate();
initializeCirviaParental();
}

private void initializeCirviaParental() {
try {
// Create configuration from BuildConfig
ParentalAIConfig config = new ParentalAIConfig(
BuildConfig.CIRVIA_API_KEY,
BuildConfig.CIRVIA_INGEST_URL,
BuildConfig.CIRVIA_AUTH_URL,
BuildConfig.DEBUG
);

// Initialize SDK
ParentalAI.init(this, config,
() -> {
isCirviaReady = true;
Log.i(TAG, "Cirvia Parental SDK initialized successfully");
},
() -> {
isCirviaReady = false;
Log.w(TAG, "Cirvia Parental SDK initialization declined by user");
}
);

} catch (Exception e) {
Log.e(TAG, "Failed to initialize Cirvia Parental SDK", e);
isCirviaReady = false;
}
}

public boolean isCirviaReady() {
return isCirviaReady;
}

public void reinitializeCirvia() {
initializeCirviaParental();
}
}

Main Activity

MainActivity.java

package com.example.safechat;

import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private static final int REQUEST_IMAGE_PICK = 1001;
private static final int REQUEST_STORAGE_PERMISSION = 1002;

private RecyclerView chatRecyclerView;
private EditText messageEditText;
private Button sendButton;
private ImageButton imageButton;

private ChatAdapter chatAdapter;
private List<ChatMessage> messages;
private ContentMonitor contentMonitor;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

initializeViews();
setupChat();
setupContentMonitoring();
}

private void initializeViews() {
chatRecyclerView = findViewById(R.id.chat_recycler_view);
messageEditText = findViewById(R.id.message_edit_text);
sendButton = findViewById(R.id.send_button);
imageButton = findViewById(R.id.image_button);

// Setup RecyclerView
messages = new ArrayList<>();
chatAdapter = new ChatAdapter(messages);
chatRecyclerView.setLayoutManager(new LinearLayoutManager(this));
chatRecyclerView.setAdapter(chatAdapter);
}

private void setupChat() {
// Send button click listener
sendButton.setOnClickListener(v -> sendMessage());

// Image share button
imageButton.setOnClickListener(v -> shareImage());

// Enable/disable send button based on text input
messageEditText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
sendButton.setEnabled(s.toString().trim().length() > 0);
}

@Override
public void afterTextChanged(Editable s) {}
});

// Initial button state
sendButton.setEnabled(false);
}

private void setupContentMonitoring() {
contentMonitor = new ContentMonitor(this);
}

private void sendMessage() {
String messageText = messageEditText.getText().toString().trim();

if (messageText.isEmpty()) {
return;
}

// Monitor message content before sending
contentMonitor.monitorTextContent("safechat-message", messageText, () -> {
// Content monitoring completed successfully
Log.d(TAG, "Message content monitored successfully");
});

// Add message to chat (simulate sending)
ChatMessage message = new ChatMessage(messageText, ChatMessage.Type.TEXT, true);
addMessageToChat(message);

// Clear input
messageEditText.setText("");

// Simulate receiving a response (for demo purposes)
simulateResponse();
}

private void shareImage() {
// Check storage permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_STORAGE_PERMISSION);
return;
}

// Open image picker
Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(intent, REQUEST_IMAGE_PICK);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);

if (requestCode == REQUEST_IMAGE_PICK && resultCode == RESULT_OK && data != null) {
Uri imageUri = data.getData();
if (imageUri != null) {
handleImageSelection(imageUri);
}
}
}

private void handleImageSelection(Uri imageUri) {
try {
// Load bitmap from URI
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);

// Monitor image content
contentMonitor.monitorImageContent("safechat-image", bitmap, () -> {
Log.d(TAG, "Image content monitored successfully");

// Add image message to chat on main thread
runOnUiThread(() -> {
ChatMessage imageMessage = new ChatMessage(imageUri.toString(), ChatMessage.Type.IMAGE, true);
addMessageToChat(imageMessage);
});
});

} catch (Exception e) {
Log.e(TAG, "Error handling image selection", e);
Toast.makeText(this, "Failed to load image", Toast.LENGTH_SHORT).show();
}
}

private void addMessageToChat(ChatMessage message) {
messages.add(message);
chatAdapter.notifyItemInserted(messages.size() - 1);
chatRecyclerView.scrollToPosition(messages.size() - 1);
}

private void simulateResponse() {
// Simulate a response message after a delay
messageEditText.postDelayed(() -> {
ChatMessage response = new ChatMessage("Thanks for your message!", ChatMessage.Type.TEXT, false);
addMessageToChat(response);
}, 1000);
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

if (requestCode == REQUEST_STORAGE_PERMISSION) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
shareImage();
} else {
Toast.makeText(this, "Storage permission required to share images", Toast.LENGTH_SHORT).show();
}
}
}
}

Content Monitoring

ContentMonitor.java

package com.example.safechat;

import android.content.Context;
import android.graphics.Bitmap;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;

import com.cirvia.parentalai.ParentalAI;
import com.example.safechat.utils.ImageEncoder;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ContentMonitor {
private static final String TAG = "ContentMonitor";
private final Context context;
private final ExecutorService executorService;
private final Handler mainHandler;

public ContentMonitor(Context context) {
this.context = context.getApplicationContext();
this.executorService = Executors.newFixedThreadPool(2);
this.mainHandler = new Handler(Looper.getMainLooper());
}

public void monitorTextContent(String platform, String content, Runnable onComplete) {
// Monitor text content with error handling
try {
// Validate inputs
if (platform == null || platform.trim().isEmpty()) {
Log.w(TAG, "Invalid platform identifier");
return;
}

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

// Check if Cirvia is ready
SafeChatApplication app = (SafeChatApplication) context.getApplicationContext();
if (!app.isCirviaReady()) {
Log.w(TAG, "Cirvia SDK not ready - attempting reinitialization");
app.reinitializeCirvia();

// Queue for retry after a delay
mainHandler.postDelayed(() ->
monitorTextContent(platform, content, onComplete), 2000);
return;
}

// Monitor content
ParentalAI.sendTextIncident(platform, content);
Log.d(TAG, "Text content monitored: " + platform);

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

} catch (IllegalStateException e) {
Log.w(TAG, "SDK not initialized - will retry", e);
handleReinitialization(platform, content, onComplete);

} catch (Exception e) {
Log.e(TAG, "Error monitoring text content", e);
// Don't break app functionality due to monitoring errors
if (onComplete != null) {
onComplete.run();
}
}
}

public void monitorImageContent(String platform, Bitmap image, Runnable onComplete) {
// Process images on background thread
executorService.submit(() -> {
try {
// Validate inputs
if (platform == null || platform.trim().isEmpty()) {
Log.w(TAG, "Invalid platform identifier for image");
return;
}

if (image == null || image.isRecycled()) {
Log.w(TAG, "Invalid image provided");
return;
}

// Check if Cirvia is ready
SafeChatApplication app = (SafeChatApplication) context.getApplicationContext();
if (!app.isCirviaReady()) {
Log.w(TAG, "Cirvia SDK not ready for image monitoring");

// Try reinitialization on main thread
mainHandler.post(() -> {
app.reinitializeCirvia();
// Retry after delay
mainHandler.postDelayed(() ->
monitorImageContent(platform, image, onComplete), 2000);
});
return;
}

// Encode image
String base64Image = ImageEncoder.encodeImageToBase64(image);

// Monitor image
ParentalAI.sendImageIncident(platform, base64Image);
Log.d(TAG, "Image content monitored: " + platform);

// Success callback on main thread
if (onComplete != null) {
mainHandler.post(onComplete);
}

} catch (OutOfMemoryError e) {
Log.e(TAG, "Out of memory processing image", e);
tryImageMonitoringWithSmallerSize(platform, image, onComplete);

} catch (Exception e) {
Log.e(TAG, "Error monitoring image content", e);
// Don't break app functionality
if (onComplete != null) {
mainHandler.post(onComplete);
}
}
});
}

private void handleReinitialization(String platform, String content, Runnable onComplete) {
SafeChatApplication app = (SafeChatApplication) context.getApplicationContext();

// Attempt reinitialization
app.reinitializeCirvia();

// Retry monitoring after delay
mainHandler.postDelayed(() -> {
try {
ParentalAI.sendTextIncident(platform, content);
if (onComplete != null) {
onComplete.run();
}
} catch (Exception e) {
Log.e(TAG, "Retry failed", e);
if (onComplete != null) {
onComplete.run();
}
}
}, 3000);
}

private void tryImageMonitoringWithSmallerSize(String platform, Bitmap original, Runnable onComplete) {
try {
// Try with smaller image size
int newWidth = original.getWidth() / 2;
int newHeight = original.getHeight() / 2;

if (newWidth > 100 && newHeight > 100) {
Bitmap smallerImage = Bitmap.createScaledBitmap(original, newWidth, newHeight, true);
String base64Image = ImageEncoder.encodeImageToBase64(smallerImage);

ParentalAI.sendImageIncident(platform, base64Image);
Log.d(TAG, "Image monitored with reduced size");

smallerImage.recycle();

if (onComplete != null) {
mainHandler.post(onComplete);
}
} else {
Log.e(TAG, "Image too small to process further");
if (onComplete != null) {
mainHandler.post(onComplete);
}
}

} catch (Exception e) {
Log.e(TAG, "Failed to monitor even with smaller size", e);
if (onComplete != null) {
mainHandler.post(onComplete);
}
}
}

public void cleanup() {
executorService.shutdown();
}
}

Utility Classes

ImageEncoder.java

package com.example.safechat.utils;

import android.graphics.Bitmap;
import android.util.Base64;
import android.util.Log;

import java.io.ByteArrayOutputStream;

public class ImageEncoder {
private static final String TAG = "ImageEncoder";
private static final int MAX_IMAGE_SIZE = 1024; // pixels
private static final int COMPRESSION_QUALITY = 80; // 0-100

public static String encodeImageToBase64(Bitmap bitmap) {
if (bitmap == null) {
throw new IllegalArgumentException("Bitmap cannot be null");
}

// Optimize image size for faster processing and smaller payload
Bitmap optimizedBitmap = optimizeImageSize(bitmap);

try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
optimizedBitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSION_QUALITY, baos);
byte[] imageBytes = baos.toByteArray();
baos.close();

String base64 = Base64.encodeToString(imageBytes, Base64.DEFAULT);
Log.d(TAG, "Image encoded to base64. Size: " + imageBytes.length + " bytes");

return base64;

} catch (Exception e) {
Log.e(TAG, "Error encoding image to base64", e);
throw new RuntimeException("Failed to encode image", e);
} finally {
// Clean up if we created a new bitmap
if (optimizedBitmap != bitmap) {
optimizedBitmap.recycle();
}
}
}

private static Bitmap optimizeImageSize(Bitmap original) {
int width = original.getWidth();
int height = original.getHeight();

// Check if resizing is needed
if (width <= MAX_IMAGE_SIZE && height <= MAX_IMAGE_SIZE) {
return original;
}

// Calculate new dimensions maintaining aspect ratio
float ratio = Math.min(
(float) MAX_IMAGE_SIZE / width,
(float) MAX_IMAGE_SIZE / height
);

int newWidth = Math.round(width * ratio);
int newHeight = Math.round(height * ratio);

Log.d(TAG, String.format("Resizing image from %dx%d to %dx%d",
width, height, newWidth, newHeight));

return Bitmap.createScaledBitmap(original, newWidth, newHeight, true);
}
}

ChatMessage.java

package com.example.safechat;

public class ChatMessage {
public enum Type {
TEXT, IMAGE
}

private final String content;
private final Type type;
private final boolean isFromUser;
private final long timestamp;

public ChatMessage(String content, Type type, boolean isFromUser) {
this.content = content;
this.type = type;
this.isFromUser = isFromUser;
this.timestamp = System.currentTimeMillis();
}

// Getters
public String getContent() { return content; }
public Type getType() { return type; }
public boolean isFromUser() { return isFromUser; }
public long getTimestamp() { return timestamp; }
}

ChatAdapter.java

package com.example.safechat;

import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.bumptech.glide.Glide;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder> {
private final List<ChatMessage> messages;
private final SimpleDateFormat timeFormat;

public ChatAdapter(List<ChatMessage> messages) {
this.messages = messages;
this.timeFormat = new SimpleDateFormat("HH:mm", Locale.getDefault());
}

@NonNull
@Override
public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_chat_message, parent, false);
return new ChatViewHolder(view);
}

@Override
public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) {
ChatMessage message = messages.get(position);
holder.bind(message);
}

@Override
public int getItemCount() {
return messages.size();
}

static class ChatViewHolder extends RecyclerView.ViewHolder {
private final TextView messageText;
private final TextView timeText;
private final ImageView messageImage;
private final View messageContainer;

public ChatViewHolder(@NonNull View itemView) {
super(itemView);
messageText = itemView.findViewById(R.id.message_text);
timeText = itemView.findViewById(R.id.time_text);
messageImage = itemView.findViewById(R.id.message_image);
messageContainer = itemView.findViewById(R.id.message_container);
}

public void bind(ChatMessage message) {
// Set timestamp
timeText.setText(new SimpleDateFormat("HH:mm", Locale.getDefault())
.format(new Date(message.getTimestamp())));

// Configure message appearance based on sender
if (message.isFromUser()) {
messageContainer.setBackgroundResource(R.drawable.bg_message_sent);
} else {
messageContainer.setBackgroundResource(R.drawable.bg_message_received);
}

// Handle different message types
if (message.getType() == ChatMessage.Type.TEXT) {
messageText.setVisibility(View.VISIBLE);
messageImage.setVisibility(View.GONE);
messageText.setText(message.getContent());
} else if (message.getType() == ChatMessage.Type.IMAGE) {
messageText.setVisibility(View.GONE);
messageImage.setVisibility(View.VISIBLE);

// Load image using Glide
Glide.with(itemView.getContext())
.load(Uri.parse(message.getContent()))
.into(messageImage);
}
}
}
}

Layout Files

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<!-- Chat Header -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
android:title="SafeChat Demo"
android:titleTextColor="@android:color/white" />

<!-- Messages RecyclerView -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/chat_recycler_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:padding="8dp" />

<!-- Message Input -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="8dp"
android:background="@color/colorInputBackground">

<ImageButton
android:id="@+id/image_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_image"
android:contentDescription="Share image" />

<EditText
android:id="@+id/message_edit_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="8dp"
android:hint="Type a message..."
android:maxLines="3"
android:background="@drawable/bg_message_input" />

<Button
android:id="@+id/send_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send"
android:enabled="false" />

</LinearLayout>

</LinearLayout>

item_chat_message.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="4dp">

<LinearLayout
android:id="@+id/message_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="64dp"
android:orientation="vertical"
android:padding="12dp"
android:background="@drawable/bg_message_sent">

<TextView
android:id="@+id/message_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@android:color/white"
android:textSize="16sp" />

<ImageView
android:id="@+id/message_image"
android:layout_width="200dp"
android:layout_height="200dp"
android:scaleType="centerCrop"
android:visibility="gone" />

<TextView
android:id="@+id/time_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@android:color/white"
android:textSize="12sp"
android:alpha="0.7" />

</LinearLayout>

</FrameLayout>

Testing the Integration

Test Cases

package com.example.safechat;

import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;

import org.junit.Test;
import org.junit.runner.RunWith;

import static org.junit.Assert.*;

@RunWith(AndroidJUnit4.class)
public class ContentMonitorTest {

@Test
public void testTextContentMonitoring() {
ContentMonitor monitor = new ContentMonitor(
InstrumentationRegistry.getInstrumentation().getTargetContext()
);

// Test normal message
monitor.monitorTextContent("test-platform", "Hello world!", () -> {
// Success callback
});

// Test empty message
monitor.monitorTextContent("test-platform", "", () -> {
// Should not break
});

// Test null message
monitor.monitorTextContent("test-platform", null, () -> {
// Should handle gracefully
});
}
}

Production Considerations

1. Security Enhancements

// Obfuscate API keys in production
public class ApiKeyManager {
public static String getApiKey() {
// In production, consider using Android Keystore or NDK for key protection
return BuildConfig.CIRVIA_API_KEY;
}

public static ParentalAIConfig getSecureConfig() {
return new ParentalAIConfig(
getApiKey(),
BuildConfig.CIRVIA_INGEST_URL,
BuildConfig.CIRVIA_AUTH_URL,
false // Disable debug in production
);
}
}

2. Performance Optimizations

public class OptimizedContentMonitor extends ContentMonitor {
private static final int MAX_QUEUE_SIZE = 50;
private final Queue<MonitoringTask> pendingTasks = new ConcurrentLinkedQueue<>();

public void monitorTextWithBatching(String platform, String content) {
// Batch multiple monitoring requests to reduce API calls
MonitoringTask task = new MonitoringTask(platform, content, System.currentTimeMillis());

if (pendingTasks.size() >= MAX_QUEUE_SIZE) {
processBatch();
}

pendingTasks.offer(task);

// Process batch after 2 seconds of inactivity
scheduleDelayedBatchProcessing();
}

private void processBatch() {
List<MonitoringTask> tasks = new ArrayList<>();
MonitoringTask task;

while ((task = pendingTasks.poll()) != null) {
tasks.add(task);
}

if (!tasks.isEmpty()) {
// Process all tasks in background
new Thread(() -> {
for (MonitoringTask t : tasks) {
super.monitorTextContent(t.platform, t.content, null);
}
}).start();
}
}
}

3. Analytics Integration

public class AnalyticsWrapper {
public static void trackMonitoringEvent(String platform, String contentType, boolean success) {
// Track monitoring metrics for debugging and optimization
Map<String, Object> properties = new HashMap<>();
properties.put("platform", platform);
properties.put("content_type", contentType);
properties.put("success", success);
properties.put("timestamp", System.currentTimeMillis());

// Send to your analytics service
// Analytics.track("cirvia_monitoring", properties);
}
}

4. Configuration Management

public class ConfigManager {
private static final String PREFS_NAME = "cirvia_config";
private final SharedPreferences preferences;

public ConfigManager(Context context) {
preferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}

public boolean isMonitoringEnabled() {
return preferences.getBoolean("monitoring_enabled", true);
}

public void setMonitoringEnabled(boolean enabled) {
preferences.edit().putBoolean("monitoring_enabled", enabled).apply();
}

public Set<String> getEnabledPlatforms() {
return preferences.getStringSet("enabled_platforms",
new HashSet<>(Arrays.asList("chat", "image", "social")));
}

public void updateEnabledPlatforms(Set<String> platforms) {
preferences.edit().putStringSet("enabled_platforms", platforms).apply();
}
}

Debugging and Monitoring

1. Debug Logging

public class CirviaDebugger {
private static final String TAG = "CirviaDebug";
private static boolean debugEnabled = BuildConfig.DEBUG;

public static void logMonitoringAttempt(String platform, String contentType) {
if (debugEnabled) {
Log.d(TAG, String.format("Monitoring %s content on %s", contentType, platform));
}
}

public static void logMonitoringSuccess(String platform, String contentType) {
if (debugEnabled) {
Log.d(TAG, String.format("Successfully monitored %s on %s", contentType, platform));
}
}

public static void logMonitoringError(String platform, String contentType, Exception error) {
Log.e(TAG, String.format("Failed to monitor %s on %s: %s",
contentType, platform, error.getMessage()), error);
}

public static void enableDebugMode(boolean enabled) {
debugEnabled = enabled;
}
}

2. Health Checks

public class CirviaHealthChecker {

public static void performHealthCheck(Context context, HealthCheckCallback callback) {
new Thread(() -> {
HealthCheckResult result = new HealthCheckResult();

// Check SDK initialization
SafeChatApplication app = (SafeChatApplication) context.getApplicationContext();
result.sdkInitialized = app.isCirviaReady();

// Check network connectivity
result.networkAvailable = NetworkUtils.isNetworkAvailable(context);

// Check API key validity
result.apiKeyValid = validateApiKey();

// Return result on main thread
new Handler(Looper.getMainLooper()).post(() -> callback.onResult(result));
}).start();
}

private static boolean validateApiKey() {
String apiKey = BuildConfig.CIRVIA_API_KEY;
return apiKey != null && apiKey.startsWith("pk_") && apiKey.length() > 20;
}

public interface HealthCheckCallback {
void onResult(HealthCheckResult result);
}

public static class HealthCheckResult {
public boolean sdkInitialized;
public boolean networkAvailable;
public boolean apiKeyValid;

public boolean isHealthy() {
return sdkInitialized && networkAvailable && apiKeyValid;
}
}
}

Error Recovery Strategies

1. Automatic Retry with Circuit Breaker

public class CircuitBreakerMonitor {
private enum State { CLOSED, OPEN, HALF_OPEN }

private State state = State.CLOSED;
private int failureCount = 0;
private long lastFailureTime = 0;
private final int failureThreshold = 5;
private final long timeoutDuration = 60000; // 1 minute

public void monitorWithCircuitBreaker(String platform, String content) {
if (state == State.OPEN) {
if (System.currentTimeMillis() - lastFailureTime > timeoutDuration) {
state = State.HALF_OPEN;
} else {
Log.w("CircuitBreaker", "Circuit breaker is OPEN - skipping monitoring");
return;
}
}

try {
ParentalAI.sendTextIncident(platform, content);

// Success - reset failure count
if (state == State.HALF_OPEN) {
state = State.CLOSED;
}
failureCount = 0;

} catch (Exception e) {
failureCount++;
lastFailureTime = System.currentTimeMillis();

if (failureCount >= failureThreshold) {
state = State.OPEN;
Log.w("CircuitBreaker", "Circuit breaker opened due to failures");
}

throw e;
}
}
}

2. Graceful Degradation

public class GracefulDegradationManager {
private boolean isFullServiceAvailable = true;
private boolean isBasicServiceAvailable = true;

public void monitorWithDegradation(String platform, String content) {
if (isFullServiceAvailable) {
try {
// Attempt full monitoring
ParentalAI.sendTextIncident(platform, content);
return;
} catch (Exception e) {
Log.w("Degradation", "Full service failed, trying basic monitoring");
isFullServiceAvailable = false;
scheduleServiceRecoveryCheck();
}
}

if (isBasicServiceAvailable) {
try {
// Fallback to basic monitoring (e.g., local keyword filtering)
performBasicContentCheck(platform, content);
return;
} catch (Exception e) {
Log.w("Degradation", "Basic service failed");
isBasicServiceAvailable = false;
scheduleServiceRecoveryCheck();
}
}

// Both services unavailable - log and continue
Log.e("Degradation", "All monitoring services unavailable");
}

private void performBasicContentCheck(String platform, String content) {
// Implement basic local content filtering as fallback
String[] problematicKeywords = {"meet", "address", "phone", "secret"};

for (String keyword : problematicKeywords) {
if (content.toLowerCase().contains(keyword)) {
Log.w("BasicMonitor", "Potentially problematic content detected locally");
// Could notify parents through alternative means
break;
}
}
}

private void scheduleServiceRecoveryCheck() {
new Handler(Looper.getMainLooper()).postDelayed(() -> {
// Reset service availability and try again
isFullServiceAvailable = true;
isBasicServiceAvailable = true;
}, 300000); // 5 minutes
}
}

User Experience Enhancements

1. Progress Indicators

public class MonitoringProgressManager {

public void showMonitoringProgress(Context context, String message) {
if (context instanceof Activity) {
Activity activity = (Activity) context;

// Show subtle progress indicator
Toast.makeText(context, "🛡️ " + message, Toast.LENGTH_SHORT).show();
}
}

public void showMonitoringComplete(Context context) {
// Optional: Show completion feedback
Log.d("Progress", "Content monitoring completed");
}
}

2. Settings Integration

public class ParentalControlsSettings {

public static void showSettingsDialog(Context context) {
AlertDialog.Builder builder = new AlertDialog.Builder(context);
View settingsView = LayoutInflater.from(context).inflate(R.layout.dialog_parental_settings, null);

Switch monitoringSwitch = settingsView.findViewById(R.id.monitoring_enabled_switch);
Switch textSwitch = settingsView.findViewById(R.id.text_monitoring_switch);
Switch imageSwitch = settingsView.findViewById(R.id.image_monitoring_switch);

ConfigManager configManager = new ConfigManager(context);

// Load current settings
monitoringSwitch.setChecked(configManager.isMonitoringEnabled());
// Set other switches based on enabled platforms...

builder.setView(settingsView)
.setTitle("Parental Controls")
.setPositiveButton("Save", (dialog, which) -> {
// Save settings
configManager.setMonitoringEnabled(monitoringSwitch.isChecked());
// Save other settings...
})
.setNegativeButton("Cancel", null)
.show();
}
}

Deployment Checklist

Pre-Production Checklist

  • API Keys Secured: Production keys stored securely, debug keys removed
  • Debug Logging Disabled: No sensitive information in production logs
  • Error Handling Tested: All error scenarios handled gracefully
  • Performance Optimized: Image processing and network calls optimized
  • Offline Support: Content queued when network unavailable
  • User Privacy: Appropriate consent flows implemented
  • Platform Compliance: Follows platform-specific guidelines
  • Testing Complete: Integration tested with real content scenarios

Monitoring and Maintenance

public class ProductionMonitoring {

public static void setupCrashReporting() {
Thread.setDefaultUncaughtExceptionHandler((thread, exception) -> {
// Log Cirvia-related crashes for debugging
if (exception.getStackTrace().toString().contains("cirvia")) {
Log.e("CirviaProduction", "Cirvia-related crash detected", exception);
// Send to crash reporting service
}
});
}

public static void trackApiMetrics() {
// Track API call success rates, response times, etc.
// Useful for monitoring service health
}
}

Conclusion

This example demonstrates a complete, production-ready integration of the Cirvia Parental SDK. Key highlights:

Complete Implementation

  • Text and image monitoring with proper error handling
  • Offline queue management and retry logic
  • User-friendly interface with progress indicators

Production-Ready Features

  • Secure API key management
  • Performance optimizations
  • Graceful degradation strategies
  • Comprehensive error recovery

Best Practices

  • Background processing for non-blocking operations
  • Proper resource management and cleanup
  • Extensive logging and debugging support
  • User privacy and consent management

Scalability Considerations

  • Circuit breaker pattern for fault tolerance
  • Batching for high-volume applications
  • Configuration management for runtime adjustments

Next Steps


Ready for production? This example provides a solid foundation for implementing Cirvia Parental monitoring in any Android application. Customize the UI and add platform-specific features as needed for your use case.