Skip to main content

Image Monitoring Guide

This guide covers best practices for monitoring image content using the Cirvia Parental SDK. Learn how to protect children from inappropriate visual content while maintaining app performance and user experience.

Overview

Image monitoring helps protect children from:

  • Inappropriate sexual content and nudity
  • Violent or graphic imagery
  • Self-harm related images and dangerous activities
  • Hate symbols and discriminatory content
  • Personal information exposure (screenshots with private data)

When to Monitor Images

High-Priority Scenarios

Monitor images in these critical situations:

// ✅ User profile pictures
public void onProfilePictureChanged(Bitmap newProfilePic) {
String base64Image = encodeImageToBase64(newProfilePic);
ParentalAI.sendImageIncident("profile-picture", base64Image);
updateProfilePicture(newProfilePic);
}

// ✅ Photos shared in chats/messages
public void onImageMessageSent(Uri imageUri) {
try {
Bitmap bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), imageUri);
String base64Image = encodeImageToBase64(bitmap);
ParentalAI.sendImageIncident("chat-image", base64Image);
sendImageMessage(imageUri);
} catch (IOException e) {
Log.e("ImageMonitor", "Failed to process image", e);
}
}

// ✅ Social media posts and stories
public void onPhotoPosted(Bitmap image) {
String base64Image = encodeImageToBase64(image);
ParentalAI.sendImageIncident("social-post", base64Image);
publishPhoto(image);
}

// ✅ Camera captures within app
public void onCameraPictureTaken(byte[] imageData) {
String base64Image = Base64.encodeToString(imageData, Base64.DEFAULT);
ParentalAI.sendImageIncident("camera-capture", base64Image);
saveOrShareImage(imageData);
}

Medium-Priority Scenarios

Consider monitoring these for comprehensive protection:

// Screenshots (may contain private information)
public void onScreenshotShared(Bitmap screenshot) {
String base64Image = encodeImageToBase64(screenshot);
ParentalAI.sendImageIncident("screenshot", base64Image);
}

// Game-related custom images
public void onCustomImageUploaded(Bitmap customImage) {
String base64Image = encodeImageToBase64(customImage);
ParentalAI.sendImageIncident("game-content", base64Image);
}

// Received images (from other users)
public void onImageReceived(String senderId, Bitmap receivedImage) {
String base64Image = encodeImageToBase64(receivedImage);
ParentalAI.sendImageIncident("received-image", base64Image);
}

Image Processing and Optimization

Image Encoding Helper

public class 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
Bitmap optimizedBitmap = optimizeImageSize(bitmap);

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

return Base64.encodeToString(imageBytes, Base64.DEFAULT);
} 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);

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

URI to Bitmap Conversion

public class ImageUtils {

public static Bitmap loadBitmapFromUri(Context context, Uri imageUri) throws IOException {
return loadBitmapFromUri(context, imageUri, MAX_IMAGE_SIZE);
}

public static Bitmap loadBitmapFromUri(Context context, Uri imageUri, int maxSize) throws IOException {
// First decode to get image dimensions
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;

InputStream inputStream = context.getContentResolver().openInputStream(imageUri);
BitmapFactory.decodeStream(inputStream, null, options);
inputStream.close();

// Calculate sample size for efficient loading
options.inSampleSize = calculateInSampleSize(options, maxSize, maxSize);
options.inJustDecodeBounds = false;

// Load the actual bitmap
inputStream = context.getContentResolver().openInputStream(imageUri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
inputStream.close();

return bitmap;
}

private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;

while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}

return inSampleSize;
}
}

Implementation Patterns

Camera Integration

public class CameraMonitor {
private static final int CAMERA_REQUEST_CODE = 1001;

public void takePicture() {
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(cameraIntent, CAMERA_REQUEST_CODE);
}
}

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

if (requestCode == CAMERA_REQUEST_CODE && resultCode == RESULT_OK) {
Bundle extras = data.getExtras();
Bitmap imageBitmap = (Bitmap) extras.get("data");

if (imageBitmap != null) {
// Monitor the captured image
monitorCapturedImage(imageBitmap);

// Continue with normal app flow
handleCapturedImage(imageBitmap);
}
}
}

private void monitorCapturedImage(Bitmap image) {
try {
String base64Image = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident("camera", base64Image);
} catch (Exception e) {
Log.e("CameraMonitor", "Failed to monitor captured image", e);
}
}
}
public class GalleryMonitor {
private static final int GALLERY_REQUEST_CODE = 2001;

public void selectImageFromGallery() {
Intent galleryIntent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
startActivityForResult(galleryIntent, GALLERY_REQUEST_CODE);
}

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

if (requestCode == GALLERY_REQUEST_CODE && resultCode == RESULT_OK && data != null) {
Uri selectedImageUri = data.getData();

if (selectedImageUri != null) {
monitorSelectedImage(selectedImageUri);
}
}
}

private void monitorSelectedImage(Uri imageUri) {
new Thread(() -> {
try {
Bitmap bitmap = ImageUtils.loadBitmapFromUri(this, imageUri);
String base64Image = ImageEncoder.encodeImageToBase64(bitmap);

// Monitor on background thread
ParentalAI.sendImageIncident("gallery-selection", base64Image);

// Update UI on main thread
runOnUiThread(() -> handleSelectedImage(bitmap));

} catch (IOException e) {
Log.e("GalleryMonitor", "Failed to load selected image", e);
runOnUiThread(() -> showError("Failed to load image"));
} catch (Exception e) {
Log.e("GalleryMonitor", "Failed to monitor selected image", e);
}
}).start();
}
}

Chat Image Sharing

public class ChatImageSharing {

public void shareImageInChat(Uri imageUri, String chatId) {
// Process image asynchronously to avoid blocking UI
new ImageProcessingTask(imageUri, chatId).execute();
}

private class ImageProcessingTask extends AsyncTask<Void, Void, String> {
private final Uri imageUri;
private final String chatId;
private Exception processingError;

public ImageProcessingTask(Uri imageUri, String chatId) {
this.imageUri = imageUri;
this.chatId = chatId;
}

@Override
protected String doInBackground(Void... voids) {
try {
// Load and encode image
Bitmap bitmap = ImageUtils.loadBitmapFromUri(ChatImageSharing.this, imageUri);
String base64Image = ImageEncoder.encodeImageToBase64(bitmap);

// Monitor the image
ParentalAI.sendImageIncident("chat-image", base64Image);

return base64Image;

} catch (Exception e) {
processingError = e;
return null;
}
}

@Override
protected void onPostExecute(String base64Image) {
if (processingError != null) {
Log.e("ChatImage", "Image processing failed", processingError);
showError("Failed to process image");
} else if (base64Image != null) {
// Send image to chat
sendImageToChat(base64Image, chatId);
}
}
}
}

Platform-Specific Monitoring

Social Media Apps

public class SocialMediaImageMonitor {

// Monitor Instagram-style story posts
public void postStoryImage(Bitmap image) {
String base64Image = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident("instagram-story", base64Image);
uploadStoryImage(image);
}

// Monitor profile picture changes
public void updateProfilePicture(Uri newPictureUri) {
new Thread(() -> {
try {
Bitmap bitmap = ImageUtils.loadBitmapFromUri(this, newPictureUri);
String base64Image = ImageEncoder.encodeImageToBase64(bitmap);
ParentalAI.sendImageIncident("instagram-profile", base64Image);

runOnUiThread(() -> setProfilePicture(bitmap));
} catch (Exception e) {
Log.e("ProfilePic", "Failed to monitor profile picture", e);
}
}).start();
}

// Monitor direct message images
public void sendDirectMessageImage(String recipientId, Bitmap image) {
String base64Image = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident("instagram-dm", base64Image);
sendDMImage(recipientId, image);
}
}

Gaming Platforms

public class GameImageMonitor {

// Monitor Roblox-style custom decals/images
public void uploadCustomDecal(Bitmap decalImage) {
String base64Image = ImageEncoder.encodeImageToBase64(decalImage);
ParentalAI.sendImageIncident("roblox-decal", base64Image);
uploadDecalToGame(decalImage);
}

// Monitor screenshot sharing in games
public void shareGameScreenshot(Bitmap screenshot) {
String base64Image = ImageEncoder.encodeImageToBase64(screenshot);
ParentalAI.sendImageIncident("roblox-screenshot", base64Image);
shareScreenshot(screenshot);
}

// Monitor avatar customization images
public void setAvatarClothing(Bitmap clothingImage) {
String base64Image = ImageEncoder.encodeImageToBase64(clothingImage);
ParentalAI.sendImageIncident("roblox-avatar", base64Image);
updateAvatarClothing(clothingImage);
}
}

Messaging Apps

public class MessagingImageMonitor {

// Monitor Discord-style image uploads
public void uploadImageToChannel(String channelId, Bitmap image) {
String base64Image = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident("discord-upload", base64Image);
sendImageToChannel(channelId, image);
}

// Monitor emoji/sticker uploads
public void uploadCustomEmoji(Bitmap emojiImage) {
String base64Image = ImageEncoder.encodeImageToBase64(emojiImage);
ParentalAI.sendImageIncident("discord-emoji", base64Image);
addCustomEmoji(emojiImage);
}
}

Performance Optimization

Image Caching and Deduplication

public class ImageMonitorCache {
private final LruCache<String, String> imageHashCache;

public ImageMonitorCache() {
// Cache up to 100 image hashes
imageHashCache = new LruCache<>(100);
}

public boolean shouldMonitorImage(Bitmap image) {
String imageHash = generateImageHash(image);

// Check if we've already monitored this image
if (imageHashCache.get(imageHash) != null) {
Log.d("ImageCache", "Skipping duplicate image");
return false;
}

// Add to cache
imageHashCache.put(imageHash, imageHash);
return true;
}

private String generateImageHash(Bitmap image) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 50, baos);
byte[] imageBytes = baos.toByteArray();

MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hashBytes = md.digest(imageBytes);

StringBuilder sb = new StringBuilder();
for (byte b : hashBytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();

} catch (Exception e) {
Log.e("ImageHash", "Failed to generate hash", e);
return String.valueOf(image.hashCode());
}
}
}

Background Processing

public class BackgroundImageMonitor {
private final ExecutorService executorService;
private final ImageMonitorCache cache;

public BackgroundImageMonitor() {
executorService = Executors.newFixedThreadPool(2);
cache = new ImageMonitorCache();
}

public void monitorImageAsync(Bitmap image, String platform) {
executorService.submit(() -> {
try {
// Check cache to avoid duplicate processing
if (!cache.shouldMonitorImage(image)) {
return;
}

// Process and monitor image
String base64Image = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident(platform, base64Image);

Log.d("ImageMonitor", "Image monitoring completed for platform: " + platform);

} catch (Exception e) {
Log.e("ImageMonitor", "Background image monitoring failed", e);
}
});
}

public void shutdown() {
executorService.shutdown();
try {
if (!executorService.awaitTermination(5, TimeUnit.SECONDS)) {
executorService.shutdownNow();
}
} catch (InterruptedException e) {
executorService.shutdownNow();
Thread.currentThread().interrupt();
}
}
}

Error Handling and Edge Cases

Robust Image Monitoring

public class RobustImageMonitor {
private static final String TAG = "ImageMonitor";
private static final int MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

public void monitorImageSafely(Uri imageUri, String platform) {
new Thread(() -> {
try {
// Validate image URI
if (!isValidImageUri(imageUri)) {
Log.w(TAG, "Invalid image URI provided");
return;
}

// Check file size
if (!isValidFileSize(imageUri)) {
Log.w(TAG, "Image file too large for monitoring");
return;
}

// Load and process image
Bitmap bitmap = ImageUtils.loadBitmapFromUri(getApplicationContext(), imageUri);

if (bitmap == null) {
Log.w(TAG, "Failed to load bitmap from URI");
return;
}

// Monitor image with retry logic
monitorWithRetry(bitmap, platform, 0);

} catch (OutOfMemoryError e) {
Log.e(TAG, "Out of memory while processing image", e);
// Try with smaller image size
tryMonitorWithSmallerSize(imageUri, platform);

} catch (IOException e) {
Log.e(TAG, "IO error while loading image", e);

} catch (Exception e) {
Log.e(TAG, "Unexpected error in image monitoring", e);
}
}).start();
}

private boolean isValidImageUri(Uri uri) {
if (uri == null) return false;

String mimeType = getContentResolver().getType(uri);
return mimeType != null && mimeType.startsWith("image/");
}

private boolean isValidFileSize(Uri uri) {
try {
ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "r");
if (pfd != null) {
long fileSize = pfd.getStatSize();
pfd.close();
return fileSize <= MAX_FILE_SIZE;
}
} catch (Exception e) {
Log.e(TAG, "Error checking file size", e);
}
return false;
}

private void monitorWithRetry(Bitmap bitmap, String platform, int attempt) {
try {
String base64Image = ImageEncoder.encodeImageToBase64(bitmap);
ParentalAI.sendImageIncident(platform, base64Image);

} catch (IllegalStateException e) {
// SDK not ready - retry
if (attempt < 3) {
new Handler(Looper.getMainLooper()).postDelayed(() ->
monitorWithRetry(bitmap, platform, attempt + 1),
1000 * (attempt + 1)
);
} else {
Log.e(TAG, "Failed to monitor image after retries", e);
}
}
}

private void tryMonitorWithSmallerSize(Uri imageUri, String platform) {
try {
// Load with smaller size to reduce memory usage
Bitmap smallBitmap = ImageUtils.loadBitmapFromUri(getApplicationContext(), imageUri, 512);

if (smallBitmap != null) {
String base64Image = ImageEncoder.encodeImageToBase64(smallBitmap);
ParentalAI.sendImageIncident(platform, base64Image);
smallBitmap.recycle();
}

} catch (Exception e) {
Log.e(TAG, "Failed to monitor with smaller size", e);
}
}
}

Privacy and Storage Considerations

Temporary Storage Management

public class SecureImageProcessor {

public void processImageSecurely(Bitmap image, String platform) {
File tempFile = null;
try {
// Create temporary file for processing
tempFile = createSecureTempFile();

// Save bitmap to temp file
saveBitmapToFile(image, tempFile);

// Process and monitor
String base64Image = encodeFileToBase64(tempFile);
ParentalAI.sendImageIncident(platform, base64Image);

} catch (Exception e) {
Log.e("SecureProcessor", "Error processing image", e);
} finally {
// Always clean up temporary files
if (tempFile != null && tempFile.exists()) {
boolean deleted = tempFile.delete();
if (!deleted) {
Log.w("SecureProcessor", "Failed to delete temp file");
}
}
}
}

private File createSecureTempFile() throws IOException {
File tempDir = new File(getCacheDir(), "image_processing");
if (!tempDir.exists()) {
tempDir.mkdirs();
}

return File.createTempFile("img_", ".tmp", tempDir);
}

private void saveBitmapToFile(Bitmap bitmap, File file) throws IOException {
FileOutputStream fos = new FileOutputStream(file);
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, fos);
} finally {
fos.close();
}
}
}

Testing Image Monitoring

Unit Testing

@RunWith(AndroidJUnit4.class)
public class ImageMonitoringTest {

@Test
public void testImageEncoding() {
// Create test bitmap
Bitmap testBitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888);
testBitmap.eraseColor(Color.RED);

// Encode to base64
String base64 = ImageEncoder.encodeImageToBase64(testBitmap);

// Verify result
assertNotNull(base64);
assertTrue(base64.length() > 0);

// Clean up
testBitmap.recycle();
}

@Test
public void testImageOptimization() {
// Create large bitmap
Bitmap largeBitmap = Bitmap.createBitmap(2000, 2000, Bitmap.Config.ARGB_8888);

// Test that optimization reduces size
String base64Large = ImageEncoder.encodeImageToBase64(largeBitmap);

// Should be optimized to smaller size
assertTrue("Image should be optimized", base64Large.length() < estimateBase64Size(2000, 2000));

largeBitmap.recycle();
}

private int estimateBase64Size(int width, int height) {
// Rough estimate: width * height * 4 bytes * 1.33 (base64 overhead)
return (int) (width * height * 4 * 1.33);
}
}

Common Pitfalls

Avoid These Mistakes

// ❌ DON'T: Load full resolution images without optimization
public void badImageMonitoring(Uri imageUri) {
Bitmap fullResBitmap = BitmapFactory.decodeFile(imageUri.getPath()); // May cause OOM
ParentalAI.sendImageIncident("platform", ImageEncoder.encodeImageToBase64(fullResBitmap));
}

// ✅ DO: Use optimized loading
public void goodImageMonitoring(Uri imageUri) {
try {
Bitmap optimizedBitmap = ImageUtils.loadBitmapFromUri(this, imageUri);
String base64 = ImageEncoder.encodeImageToBase64(optimizedBitmap);
ParentalAI.sendImageIncident("platform", base64);
optimizedBitmap.recycle();
} catch (Exception e) {
Log.e("ImageMonitor", "Error monitoring image", e);
}
}

// ❌ DON'T: Block UI thread with image processing
public void badImageProcessing(Bitmap image) {
String base64 = ImageEncoder.encodeImageToBase64(image); // Heavy operation on UI thread
ParentalAI.sendImageIncident("platform", base64);
}

// ✅ DO: Process images on background thread
public void goodImageProcessing(Bitmap image) {
new Thread(() -> {
try {
String base64 = ImageEncoder.encodeImageToBase64(image);
ParentalAI.sendImageIncident("platform", base64);
} catch (Exception e) {
Log.e("ImageMonitor", "Error processing image", e);
}
}).start();
}

Best Practices Summary

  1. Optimize images - Resize before encoding to improve performance
  2. Process asynchronously - Never block the UI thread with image operations
  3. Handle memory carefully - Recycle bitmaps and manage memory usage
  4. Cache intelligently - Avoid processing duplicate images
  5. Validate inputs - Check file size, format, and URI validity
  6. Handle errors gracefully - Don't let image monitoring break app functionality
  7. Clean up resources - Delete temporary files and close streams
  8. Test with real images - Use various formats, sizes, and edge cases

Next Steps


Ready to protect children from inappropriate visual content? Continue to Platform Integration for platform-specific implementation examples.