Skip to main content

Overview

PhoneClaw includes a cron-based task scheduler that enables automation scripts to run on recurring schedules. This allows you to automate posting, data scraping, monitoring, and other tasks without manual intervention.

Architecture

Data Model

From MainActivity.kt:152-159:
data class CronTask(
    val id: String,
    val taskDescription: String,
    val cronExpression: String,
    val createdAt: Long = System.currentTimeMillis(),
    var lastExecuted: Long = 0L,
    var isActive: Boolean = true
)
Fields:
  • id: Unique UUID for the task
  • taskDescription: JavaScript code or human-readable description
  • cronExpression: Cron schedule (simplified format)
  • createdAt: Timestamp when task was created
  • lastExecuted: Last execution timestamp
  • isActive: Whether task should run

Scheduler Implementation

PhoneClaw uses Kotlin coroutines for lightweight background scheduling:
private val cronScheduler: ScheduledExecutorService = Executors.newScheduledThreadPool(2)
private var cronCheckJob: Job? = null
private val cronTasks = mutableMapOf<String, CronTask>()

Starting the Scheduler

From MainActivity.kt:3497-3520:
private fun startCronChecker() {
    cronCheckJob?.cancel()
    
    cronCheckJob = mainScope.launch {
        Log.d("MainActivity", "Cron checker started")
        
        while (isActive && !isDestroyed) {
            try {
                checkAndExecuteCronTasks()
                
                // Check every 60 seconds
                delay(60000)
            } catch (e: CancellationException) {
                Log.d("MainActivity", "Cron checker cancelled")
                throw e
            } catch (e: Exception) {
                Log.e("MainActivity", "Error in cron checker: ${e.message}")
                delay(60000)
            }
        }
    }
}
The scheduler checks every 60 seconds to see if any tasks should execute. This provides minute-level scheduling precision.

Task Execution Logic

From MainActivity.kt:3521-3555:
private suspend fun checkAndExecuteCronTasks() {
    if (isDestroyed) return
    
    val currentTime = System.currentTimeMillis()
    
    for ((taskId, cronTask) in cronTasks.toMap()) {
        if (!cronTask.isActive) continue
        
        try {
            if (shouldExecuteCronTask(cronTask, currentTime)) {
                Log.d("MainActivity", "Executing cron task: ${cronTask.taskDescription}")
                updateStatusWithAnimation("⏰ Running scheduled task")
                
                withContext(Dispatchers.Main) {
                    speakText("Running scheduled task: ${cronTask.taskDescription}")
                }
                
                // Execute the task
                executeGeneratedCode(cronTask.taskDescription)
                
                // Update last execution time
                cronTasks[taskId] = cronTask.copy(lastExecuted = currentTime)
                saveCronTasks()
                
                Log.d("MainActivity", "Cron task completed: ${cronTask.taskDescription}")
            }
        } catch (e: Exception) {
            Log.e("MainActivity", "Error executing cron task $taskId: ${e.message}")
        }
    }
}

Cron Expression Format

PhoneClaw uses a simplified cron format:
[second] [minute] [hour] [day] [month]

Supported Patterns

*/30 * * * *    # Every 30 seconds
*/10 * * * *    # Every 10 seconds
0 */5 * * *     # Every 5 minutes
0 */15 * * *    # Every 15 minutes
0 */30 * * *    # Every 30 minutes
0 0 */2 * *     # Every 2 hours
0 0 */6 * *     # Every 6 hours
0 0 0 * *       # Midnight every day
0 0 9 * *       # 9 AM every day
0 0 18 * *      # 6 PM every day

Expression Parser

From MainActivity.kt:3556-3620:
private fun shouldExecuteCronTask(cronTask: CronTask, currentTime: Long): Boolean {
    val interval = getIntervalFromCron(cronTask.cronExpression) ?: return false
    
    if (cronTask.lastExecuted == 0L) {
        return true  // Never executed, run now
    }
    
    val timeSinceLastRun = currentTime - cronTask.lastExecuted
    return timeSinceLastRun >= interval
}

private fun getIntervalFromCron(cronExpression: String): Long? {
    try {
        val parts = cronExpression.split(" ")
        if (parts.size != 5) return null
        
        val (secondPart, minutePart, hourPart, dayPart, monthPart) = parts
        
        return when {
            // Every N seconds: */30 * * * *
            secondPart.startsWith("*/") -> {
                val interval = secondPart.substring(2).toIntOrNull()
                interval?.let { it * 1000L }
            }
            
            // Every N minutes: 0 */5 * * *
            secondPart == "0" && minutePart.startsWith("*/") -> {
                val interval = minutePart.substring(2).toIntOrNull()
                interval?.let { it * 60 * 1000L }
            }
            
            // Every N hours: 0 0 */2 * *
            secondPart == "0" && minutePart == "0" && hourPart.startsWith("*/") -> {
                val interval = hourPart.substring(2).toIntOrNull()
                interval?.let { it * 60 * 60 * 1000L }
            }
            
            // Daily: 0 0 0 * *
            secondPart == "0" && minutePart == "0" && hourPart != "*" && dayPart == "*" -> {
                24 * 60 * 60 * 1000L
            }
            
            else -> null
        }
    } catch (e: Exception) {
        return null
    }
}
The parser converts cron expressions to millisecond intervals. More complex expressions (specific days, months) are not currently supported.

Creating Scheduled Tasks

Via JavaScript API

// Schedule a task to run every 5 minutes
schedule(
    "magicClicker('refresh button')",
    "0 */5 * * *"
);

// Schedule daily Instagram post at 9 AM
schedule(
    `launchInstagram();
     delay(3000);
     magicClicker('create post');
     // ... rest of upload sequence
    `,
    "0 0 9 * *"
);

// Clear all scheduled tasks
clearSchedule();

Via Kotlin API

From MainActivity.kt:3622-3633:
private fun addCronTask(taskDescription: String, cronExpression: String): String {
    val taskId = UUID.randomUUID().toString()
    val cronTask = CronTask(taskId, taskDescription, cronExpression)
    
    cronTasks[taskId] = cronTask
    saveCronTasks()
    
    val interval = getIntervalFromCron(cronExpression)
    Log.d("MainActivity", "Added cron task: $taskDescription with expression: $cronExpression, interval: ${interval}ms")
    
    return taskId
}

Via AndroidJSInterface

From MainActivity.kt:5480-5530:
@JavascriptInterface
fun schedule(task: String, cronExpression: String) {
    try {
        val taskId = this@MainActivity.addCronTask(task, cronExpression)
        
        val interval = this@MainActivity.getIntervalFromCron(cronExpression)
        val intervalText = when {
            interval == null -> "unknown interval"
            interval < 60000 -> "${interval / 1000} seconds"
            interval < 3600000 -> "${interval / 60000} minutes"
            interval < 86400000 -> "${interval / 3600000} hours"
            else -> "${interval / 86400000} days"
        }
        
        this@MainActivity.speakText("Scheduled task to run every $intervalText")
        
        Log.d("AndroidJSInterface", "Scheduled task: $task")
        Log.d("AndroidJSInterface", "Cron expression: $cronExpression")
        Log.d("AndroidJSInterface", "Interval: $intervalText")
        Log.d("AndroidJSInterface", "Total active cron tasks: ${this@MainActivity.cronTasks.size}")
    } catch (e: Exception) {
        Log.e("AndroidJSInterface", "Error scheduling task: ${e.message}")
        this@MainActivity.speakText("Error scheduling task")
    }
}

@JavascriptInterface
fun clearSchedule() {
    val taskCount = this@MainActivity.cronTasks.size
    this@MainActivity.cronTasks.clear()
    this@MainActivity.saveCronTasks()
    this@MainActivity.speakText("Cleared $taskCount scheduled tasks")
    Log.d("AndroidJSInterface", "Cleared all scheduled tasks")
}

Persistence

Tasks are saved to SharedPreferences as JSON:

Saving Tasks

private fun saveCronTasks() {
    try {
        val jsonObject = JSONObject()
        for ((key, task) in cronTasks) {
            val taskJson = JSONObject().apply {
                put("id", task.id)
                put("taskDescription", task.taskDescription)
                put("cronExpression", task.cronExpression)
                put("createdAt", task.createdAt)
                put("lastExecuted", task.lastExecuted)
                put("isActive", task.isActive)
            }
            jsonObject.put(key, taskJson)
        }
        sharedPreferences.edit().putString("cron_tasks", jsonObject.toString()).apply()
    } catch (e: Exception) {
        Log.e("MainActivity", "Error saving cron tasks: ${e.message}")
    }
}

Loading Tasks

private fun loadCronTasks() {
    try {
        val tasksJson = sharedPreferences.getString("cron_tasks", "") ?: ""
        if (tasksJson.isNotEmpty()) {
            val jsonObject = JSONObject(tasksJson)
            
            val keys = jsonObject.keys()
            while (keys.hasNext()) {
                val key = keys.next()
                val taskJson = jsonObject.getJSONObject(key)
                val task = CronTask(
                    id = taskJson.getString("id"),
                    taskDescription = taskJson.getString("taskDescription"),
                    cronExpression = taskJson.getString("cronExpression"),
                    createdAt = taskJson.optLong("createdAt", System.currentTimeMillis()),
                    lastExecuted = taskJson.optLong("lastExecuted", 0L),
                    isActive = taskJson.optBoolean("isActive", true)
                )
                
                cronTasks[task.id] = task
            }
            
            Log.d("MainActivity", "Loaded ${cronTasks.size} cron tasks")
        }
    } catch (e: Exception) {
        Log.e("MainActivity", "Error loading cron tasks: ${e.message}")
    }
}

UI Integration

PhoneClaw includes a modern UI for managing scheduled tasks:

Task List Fragment

class ScheduledTasksFragment : Fragment() {
    private lateinit var tasksRecyclerView: RecyclerView
    private lateinit var swipeRefresh: SwipeRefreshLayout
    private lateinit var tasksAdapter: TasksAdapter
    
    fun updateTasks(tasks: List<CronTask>) {
        if (::tasksAdapter.isInitialized) {
            tasksAdapter.updateTasks(tasks)
        }
    }
}

Task Management

fun runTaskNow(task: CronTask) {
    mainScope.launch {
        try {
            updateStatusWithAnimation("▶️ Running task immediately")
            speakText("Running task: ${task.taskDescription.take(50)}")
            
            executeGeneratedCode(task.taskDescription)
            
            cronTasks[task.id] = task.copy(lastExecuted = System.currentTimeMillis())
            saveCronTasks()
            updateUI()
            
            speakText("Task completed successfully")
        } catch (e: Exception) {
            Log.e("MainActivity", "Error running task now: ${e.message}")
            speakText("Error running task")
        }
    }
}

fun deleteTask(task: CronTask) {
    MaterialAlertDialogBuilder(this)
        .setTitle("Delete Task?")
        .setMessage("Are you sure you want to delete this scheduled task?")
        .setPositiveButton("Delete") { _, _ ->
            cronTasks.remove(task.id)
            saveCronTasks()
            updateUI()
            speakText("Task deleted")
        }
        .setNegativeButton("Cancel", null)
        .show()
}

Example Use Cases

Daily Social Media Posting

// Schedule TikTok upload every day at 6 PM
schedule(`
    launchTikTok();
    delay(5000);
    
    clickVideoUploadButton();
    delay(2000);
    
    clickFirstGalleryItem();
    delay(3000);
    
    magicClicker('next button');
    delay(2000);
    
    simulateTypeInFirstEditableField('#automation #dailypost');
    delay(2000);
    
    magicClicker('post button');
    speakText('Daily TikTok post scheduled!');
`, "0 0 18 * *");

Periodic Data Scraping

// Check follower count every hour
schedule(`
    launchInstagram();
    delay(3000);
    
    magicClicker('profile tab');
    delay(2000);
    
    const followers = magicScraper('follower count');
    const timestamp = new Date().toLocaleString();
    
    speakText('Current followers: ' + followers);
    
    // Log to Firebase or local storage
    Android.logInfo('FollowerTracking', timestamp + ': ' + followers);
`, "0 0 * * *");

System Monitoring

// Check battery level every 15 minutes
schedule(`
    const battery = magicScraper('battery percentage');
    const batteryNum = parseInt(battery);
    
    if (batteryNum < 20) {
        speakText('Warning: Battery low at ' + battery + ' percent');
        
        // Enable battery saver
        Android.openBatterySettings();
    }
`, "0 */15 * * *");

Automated Engagement

// Like posts every 30 minutes
schedule(`
    launchInstagram();
    delay(3000);
    
    // Scroll through feed
    for (let i = 0; i < 5; i++) {
        magicClicker('like button');
        delay(2000);
        
        swipeUp();
        delay(3000);
    }
    
    speakText('Engagement round completed');
`, "0 */30 * * *");

Best Practices

Rate Limiting: Be mindful of API rate limits and app usage policies. Excessive automation may trigger spam detection on social platforms.
Battery Optimization: Android may kill background processes to save battery. Consider:
  • Disabling battery optimization for PhoneClaw
  • Using longer intervals (hourly vs. every minute)
  • Scheduling during device charging times

Error Recovery

function robustScheduledTask() {
    try {
        // Main task logic
        launchInstagram();
        delay(3000);
        
        if (!isTextPresentOnScreen("Home")) {
            throw new Error("Instagram not loaded");
        }
        
        magicClicker("create post");
        // ... rest of automation
        
    } catch (error) {
        speakText("Task failed: " + error.message);
        Android.logInfo("ScheduledTaskError", error.message);
        
        // Attempt recovery
        speakText("Will retry next scheduled run");
    }
}

schedule(robustScheduledTask.toString(), "0 0 */6 * *");

Troubleshooting

Tasks Not Running

private fun testCronScheduler() {
    mainScope.launch {
        delay(5000)
        Log.d("MainActivity", "Testing cron scheduler...")
        Log.d("MainActivity", "Current tasks: ${cronTasks.size}")
        
        cronTasks.values.forEach { task ->
            Log.d("MainActivity", "Task: ${task.taskDescription}")
            Log.d("MainActivity", "Expression: ${task.cronExpression}")
            Log.d("MainActivity", "Last executed: ${task.lastExecuted}")
            Log.d("MainActivity", "Active: ${task.isActive}")
            
            val interval = getIntervalFromCron(task.cronExpression)
            Log.d("MainActivity", "Calculated interval: ${interval}ms")
            
            val shouldExecute = shouldExecuteCronTask(task, System.currentTimeMillis())
            Log.d("MainActivity", "Should execute now: $shouldExecute")
        }
    }
}

Check Scheduler Status

// Log current scheduled tasks
Android.logInfo("Scheduler", "Checking task status...");

// Verify scheduler is running
if (!isTextPresentOnScreen("Scheduled Tasks")) {
    speakText("Navigate to Scheduled Tasks tab to view active tasks");
}

Performance Considerations

  • 60-second check interval provides minute-level precision
  • Coroutine-based for lightweight background execution
  • Persisted to SharedPreferences for survival across app restarts
  • Automatic cleanup when app is destroyed

Lifecycle Management

override fun onDestroy() {
    isDestroyed = true
    
    // Cancel cron checker
    cronCheckJob?.cancel()
    
    // Shutdown executor
    cronScheduler.shutdownNow()
    cronScheduler.awaitTermination(1, TimeUnit.SECONDS)
    
    super.onDestroy()
}

Next Steps

ClawScript API

Full JavaScript automation reference

Vision Targeting

Use vision AI in scheduled tasks