How to Build a Scalable Android Ad‑Monitoring System with Multi‑Device Automation
This article details the design and implementation of an Android ad‑monitoring platform that controls multiple devices concurrently, automates app interactions, uses OCR for ad detection, and provides real‑time status monitoring via a floating window, while covering architecture, core modules, communication strategies, and performance optimizations.
Project Background and Overall Architecture
Manual ad monitoring is inefficient, especially with the rapid growth of mobile apps. The Android Ad Monitoring System was created to automate detection of splash and feed ads, improve accuracy, performance, and cross‑device compatibility, and provide real‑time status feedback.
Core Functional Modules
1. Device Management and Multi‑Process Concurrency
The system uses Python’s multiprocessing module to launch a separate process for each connected Android device, ensuring independent, non‑blocking ad‑monitoring tasks.
# === Startup entry: multi‑device concurrent execution ===
if __name__ == "__main__":
# Get all connected devices
all_devices = utils.get_connected_devices()
if not all_devices:
print("❌ No physical devices detected")
exit(1)
# Create a process per device
processes = []
for device_id in all_devices:
p = Process(target=ad_monitor.run_on_device, args=(device_id,))
p.start()
processes.append(p)
# Wait for all processes to finish
for p in processes:
p.join()This multi‑process approach avoids the Global Interpreter Lock (GIL) and provides better stability than multi‑threading for I/O‑heavy operations.
2. Device Control and Application Automation
ADB is used for low‑level device actions (wake, unlock, launch apps). The uiautomator2 library handles in‑app interactions such as swipes and clicks.
# Device operation methods
def wake_and_unlock(device_id):
subprocess.run(["adb", "-s", device_id, "shell", "input", "keyevent", "224"])
subprocess.run(["adb", "-s", device_id, "shell", "wm", "dismiss-keyguard"])
def lock_screen(device_id):
subprocess.run(["adb", "-s", device_id, "shell", "input", "keyevent", "26"], stdout=subprocess.DEVNULL) def scroll_half_screen(device):
try:
size = device.window_size()
start_x = size[0] // 2
start_y = size[1] * 3 // 4
end_y = size[1] // 4
device.swipe(start_x, start_y, start_x, end_y, 0.2)
time.sleep(1)
except Exception as e:
print(f"⚠️ Swipe failed: {e}")3. Ad Recognition with OCR
Captured screenshots are processed by an OCR engine; extracted text is matched against predefined keyword lists to determine whether an ad is present.
# OCR detection function
def ocr_detect_ad(image_path, keywords):
try:
text = fuOCR.getTextFromImage(image_path)
for keyword in keywords:
if keyword in str(text):
return True, keyword, str(text)
return False, None, None
except Exception as e:
print(f"⚠️ OCR failed: {e}")
return False, None, None # Splash ad detection example
def capture_splash_ad(device, app_name, save_dir, i, log_file, device_id):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
temp_filepath = os.path.join(save_dir, f"{app_name}_splash_temp_{timestamp}_{i+1}.png")
log_message(log_file, f"📸 Screenshot saved: {temp_filepath}")
device.screenshot(temp_filepath)
is_ad, keyword, txt = ocr_detect_ad(temp_filepath, config.SPLASH_AD_KEYWORDS)
if is_ad:
log_message(log_file, f"🔍 OCR text: {txt}")
notify_status(device_id, "found_ad", f"Found splash AD in {app_name}")
upload_and_delete(temp_filepath, {"media": app_name, "position_type": "splash", "os": "Android", "detected_keyword": keyword}, log_file)
log_message(log_file, f"✅ Detected splash ad (keyword: {keyword}) (attempt {i+1})")
return True
else:
log_message(log_file, f"⚠️ No splash ad detected (attempt {i+1})")
try:
os.remove(temp_filepath)
except Exception as e:
log_message(log_file, f"⚠️ Failed to delete temp file: {e}")
return False4. Data Flow Management and File System Design
Screenshots and logs are stored in a layered directory structure organized by device model and timestamp. Automatic cleanup removes files older than a configurable retention period.
# Path configuration
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(BASE_DIR, "./Android_log")
SAVE_ROOT = os.path.join(BASE_DIR, "./Android_screenshort")
TEMP_DIR = os.path.join(SAVE_ROOT, "temp")
RETENTION_TIME = timedelta(days=2) # Production setting5. Status Monitoring and Cross‑Process Communication
A floating window on each device displays real‑time status (idle, detecting, ad found, uploading, completed, error). Communication between the Python monitoring script and the Android overlay uses ADB broadcast as the primary channel, falling back to a lightweight socket when needed.
# Send status notification via ADB broadcast (fallback to socket)
def notify_status(device_id, status, message):
try:
status_message = f"STATUS|{device_id}|{status}|{message}"
broadcast_cmd = ["adb", "-s", device_id, "shell", "am", "broadcast", "-a", "com.monitor.statuswindow.UPDATE_STATUS", "--es", "message", status_message]
result = subprocess.run(broadcast_cmd, capture_output=True, text=True)
if "Broadcasting: Intent" in result.stdout:
print(f"✅ Sent broadcast: {status_message}")
return True
else:
print("⚠️ Broadcast failed, trying socket")
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.settimeout(1)
s.connect(("localhost", 8888))
s.sendall(status_message.encode('utf-8'))
print(f"✅ Sent socket message: {status_message}")
return True
except Exception as e:
print(f"❌ Socket send failed: {e}")
return False
except Exception as e:
print(f"❌ Notification error: {e}")
return False // Android side: BroadcastReceiver (Kotlin)
class StatusBroadcastReceiver(private val statusListener: StatusServer.StatusListener) : BroadcastReceiver() {
private val TAG = "StatusBroadcastReceiver"
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == "com.monitor.statuswindow.UPDATE_STATUS") {
// Extract and handle status message
}
}
}6. Floating Window Design (Android Overlay)
The overlay is built with a Service + WindowManager architecture, using TYPE_APPLICATION_OVERLAY on Android 8.0+ and TYPE_PHONE on older versions. It supports drag‑to‑move, click‑to‑open the main UI, and updates its appearance based on the current status.
// Initialize floating view (Kotlin)
private fun initFloatingView() {
windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
floatingView = LayoutInflater.from(this).inflate(R.layout.floating_window, null)
val layoutParams = WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY else WindowManager.LayoutParams.TYPE_PHONE,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
PixelFormat.TRANSLUCENT
)
layoutParams.gravity = Gravity.TOP or Gravity.START
layoutParams.x = 0
layoutParams.y = 100
// Add view and set touch listeners …
} // Touch handling for drag and click (Kotlin)
floatingView.setOnTouchListener { _, event ->
when (event.action) {
MotionEvent.ACTION_DOWN -> {
initialX = event.rawX.toInt()
initialY = event.rawY.toInt()
lastX = initialX
lastY = initialY
}
MotionEvent.ACTION_MOVE -> {
val dx = event.rawX.toInt() - lastX
val dy = event.rawY.toInt() - lastY
layoutParams.x += dx
layoutParams.y += dy
windowManager.updateViewLayout(floatingView, layoutParams)
lastX = event.rawX.toInt()
lastY = event.rawY.toInt()
}
MotionEvent.ACTION_UP -> {
val distanceX = Math.abs(event.rawX.toInt() - initialX)
val distanceY = Math.abs(event.rawY.toInt() - initialY)
if (distanceX < 10 && distanceY < 10) {
val intent = Intent(this, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
}
}
}
true
}7. System Scheduling and Workflow
The workflow for each app includes device preparation, app launch, splash‑ad detection, feed‑ad detection with simulated scrolling, optional channel switching, result handling (save & upload), and device cleanup. Detailed logging and status updates keep operators informed.
8. Optimization and Performance Tuning
Key improvements include faster device detection, pre‑loading OCR models, asynchronous initialization, aggressive temporary‑file cleanup, and a hybrid communication scheme that reduces CPU and memory usage. Reported gains: ~40% faster startup, ~30% lower memory footprint, ~25% reduced CPU load, and smoother operation on low‑end Windows machines.
9. Future Directions
Integrate deep‑learning‑based ad recognition to reduce reliance on keyword matching.
Add real‑time analytics for collected ad data.
Migrate to a cloud‑native architecture for scalability.
Develop a mobile management app for remote control.
Extend support to iOS and other platforms.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
