diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 497e51d47..edbb12da4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -86,6 +86,9 @@
+
diff --git a/app/src/main/java/com/termux/api/JobSchedulerAPI.java b/app/src/main/java/com/termux/api/JobSchedulerAPI.java
new file mode 100644
index 000000000..fe92f705b
--- /dev/null
+++ b/app/src/main/java/com/termux/api/JobSchedulerAPI.java
@@ -0,0 +1,137 @@
+package com.termux.api;
+
+import android.app.job.JobInfo;
+import android.app.job.JobScheduler;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.media.AudioManager;
+import android.os.PersistableBundle;
+import android.os.Vibrator;
+import android.util.Log;
+
+import com.termux.api.util.ResultReturner;
+
+import java.io.File;
+import java.io.PrintWriter;
+
+public class JobSchedulerAPI {
+
+ private static final String LOG_TAG = "JobSchedulerAPI";
+
+
+ static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) {
+
+ final String scriptPath = intent.getStringExtra("script");
+
+ final int periodicMillis = intent.getIntExtra("period_ms", 0);
+ final int jobId = intent.getIntExtra("job_id", 0);
+ final String networkType = intent.getStringExtra("network");
+ final boolean batteryNotLow = intent.getBooleanExtra("battery_not_low", true);
+ final boolean charging = intent.getBooleanExtra("charging", false);
+ final boolean idle = intent.getBooleanExtra("idle", false);
+ final boolean storageNotLow = intent.getBooleanExtra("storage_not_low", false);
+
+ int networkTypeCode = JobInfo.NETWORK_TYPE_NONE;
+ if (networkType != null) {
+ switch (networkType) {
+ case "any":
+ networkTypeCode = JobInfo.NETWORK_TYPE_ANY;
+ break;
+ case "unmetered":
+ networkTypeCode = JobInfo.NETWORK_TYPE_UNMETERED;
+ break;
+ case "cellular":
+ networkTypeCode = JobInfo.NETWORK_TYPE_CELLULAR;
+ break;
+ case "not_roaming":
+ networkTypeCode = JobInfo.NETWORK_TYPE_NOT_ROAMING;
+ break;
+ default:
+ case "none":
+ networkTypeCode = JobInfo.NETWORK_TYPE_NONE;
+ break;
+ }
+ }
+ if (scriptPath == null) {
+ ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultWriter() {
+ @Override
+ public void writeResult(PrintWriter out) {
+ out.println("No script path given");
+ }
+ });
+ return;
+ }
+ final File file = new File(scriptPath);
+ final String fileCheckMsg;
+ if (!file.isFile()) {
+ fileCheckMsg = "No such file: %s";
+ } else if (!file.canRead()) {
+ fileCheckMsg = "Cannot read file: %s";
+ } else if (!file.canExecute()) {
+ fileCheckMsg = "Cannot execute file: %s";
+ } else {
+ fileCheckMsg = "";
+ }
+
+ if (!fileCheckMsg.isEmpty()) {
+ ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultWriter() {
+ @Override
+ public void writeResult(PrintWriter out) {
+ out.println(String.format(fileCheckMsg, scriptPath));
+ }
+ });
+ return;
+ }
+
+ PersistableBundle extras = new PersistableBundle();
+ extras.putString(SchedulerJobService.SCRIPT_FILE_PATH, file.getAbsolutePath());
+
+
+ JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ // Display pending jobs
+ for (JobInfo job : jobScheduler.getAllPendingJobs()) {
+ final JobInfo j = job;
+ ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultWriter() {
+ @Override
+ public void writeResult(PrintWriter out) {
+ out.println(String.format("Pending job %d %s", j.getId(), j.toString()));
+ }
+ });
+ }
+
+ ComponentName serviceComponent = new ComponentName(context, SchedulerJobService.class);
+ JobInfo.Builder builder = null;
+ ;
+ builder = new JobInfo.Builder(jobId, serviceComponent)
+ .setExtras(extras)
+ .setRequiredNetworkType(networkTypeCode)
+ .setRequiresCharging(charging)
+ .setRequiresDeviceIdle(idle);
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ builder = builder.setRequiresBatteryNotLow(batteryNotLow);
+ builder = builder.setRequiresStorageNotLow(storageNotLow);
+ }
+
+ if (periodicMillis > 0) {
+ builder = builder.setPeriodic(periodicMillis);
+ }
+
+ JobInfo job = builder.build();
+
+ final int scheduleResponse = jobScheduler.schedule(job);
+
+ Log.i(LOG_TAG, String.format("Scheduled job %d to call %s every %d ms - response %d",
+ jobId, scriptPath, periodicMillis, scheduleResponse));
+ ResultReturner.returnData(apiReceiver, intent, new ResultReturner.ResultWriter() {
+ @Override
+ public void writeResult(PrintWriter out) {
+ out.println(String.format("Scheduled job %d to call %s every %d ms - response %d",
+ jobId, scriptPath, periodicMillis, scheduleResponse));
+ }
+ });
+
+ }
+
+}
diff --git a/app/src/main/java/com/termux/api/SchedulerJobService.java b/app/src/main/java/com/termux/api/SchedulerJobService.java
new file mode 100644
index 000000000..65594a7c6
--- /dev/null
+++ b/app/src/main/java/com/termux/api/SchedulerJobService.java
@@ -0,0 +1,50 @@
+package com.termux.api;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.os.PersistableBundle;
+import android.util.Log;
+
+public class SchedulerJobService extends JobService {
+
+ private static final String LOG_TAG = "TermuxAPISchedulerJob";
+ public static final String SCRIPT_FILE_PATH = "com.termux.api.jobscheduler_script_path";
+
+ // Constants from TermuxService.
+ private static final String TERMUX_SERVICE = "com.termux.app.TermuxService";
+ private static final String ACTION_EXECUTE = "com.termux.service_execute";
+ private static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
+
+ @Override
+ public boolean onStartJob(JobParameters params) {
+
+ Log.i(LOG_TAG, "Starting job " + params.toString());
+ PersistableBundle extras = params.getExtras();
+ String filePath = extras.getString(SCRIPT_FILE_PATH);
+
+ Uri scriptUri = new Uri.Builder().scheme("com.termux.file").path(filePath).build();
+ Intent executeIntent = new Intent(ACTION_EXECUTE, scriptUri);
+ executeIntent.setClassName("com.termux", TERMUX_SERVICE);
+ executeIntent.putExtra(EXTRA_EXECUTE_IN_BACKGROUND, true);
+
+ Context context = getApplicationContext();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ // https://developer.android.com/about/versions/oreo/background.html
+ context.startForegroundService(executeIntent);
+ } else {
+ context.startService(executeIntent);
+ }
+
+ Log.i(LOG_TAG, "Started job " + params.toString());
+ return false;
+ }
+
+ @Override
+ public boolean onStopJob(JobParameters params) {
+ Log.i(LOG_TAG, "Stopped job " + params.toString());
+ return false;
+ }
+}
diff --git a/app/src/main/java/com/termux/api/TermuxApiReceiver.java b/app/src/main/java/com/termux/api/TermuxApiReceiver.java
index 379b4fd2a..8f0a3cabf 100644
--- a/app/src/main/java/com/termux/api/TermuxApiReceiver.java
+++ b/app/src/main/java/com/termux/api/TermuxApiReceiver.java
@@ -83,6 +83,9 @@ public void onReceive(Context context, Intent intent) {
InfraredAPI.onReceiveTransmit(this, context, intent);
}
break;
+ case "JobScheduler":
+ JobSchedulerAPI.onReceive(this, context, intent);
+ break;
case "Keystore":
KeystoreAPI.onReceive(this, intent);
break;