diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java new file mode 100644 index 000000000..8d4ea6cb7 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/Scheduler.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2020 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +package org.strongswan.android.logic; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build; + +import java.util.ArrayList; +import java.util.PriorityQueue; +import java.util.UUID; + +import androidx.annotation.RequiresApi; + +public class Scheduler extends BroadcastReceiver +{ + private final String EXECUTE_JOB = "org.strongswan.android.Scheduler.EXECUTE_JOB"; + private final Context mContext; + private final AlarmManager mManager; + private final PriorityQueue mJobs; + + public Scheduler(Context context) + { + mContext = context; + mManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); + mJobs = new PriorityQueue<>(); + + IntentFilter filter = new IntentFilter(); + filter.addAction(EXECUTE_JOB); + mContext.registerReceiver(this, filter); + } + + /** + * Remove all pending jobs and unregister the receiver. + * Called via JNI. + */ + public void Terminate() + { + synchronized (this) + { + mJobs.clear(); + } + mManager.cancel(createIntent()); + mContext.unregisterReceiver(this); + } + + /** + * Allocate a job ID. Called via JNI. + * + * @return random ID for a new job + */ + public String allocateId() + { + return UUID.randomUUID().toString(); + } + + /** + * Create a pending intent to execute a job. + * + * @return pending intent + */ + private PendingIntent createIntent() + { + /* using component/class doesn't work with dynamic broadcast receivers */ + Intent intent = new Intent(EXECUTE_JOB); + intent.setPackage(mContext.getPackageName()); + return PendingIntent.getBroadcast(mContext, 0, intent, 0); + } + + /** + * Schedule executing a job in the future. + * Called via JNI from different threads. + * + * @param id job ID + * @param ms delta in milliseconds when the job should be executed + */ + @RequiresApi(api = Build.VERSION_CODES.M) + public void scheduleJob(String id, long ms) + { + synchronized (this) + { + ScheduledJob job = new ScheduledJob(id, System.currentTimeMillis() + ms); + mJobs.add(job); + + if (job == mJobs.peek()) + { /* update the alarm if the job has to be executed before all others */ + PendingIntent pending = createIntent(); + mManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, job.Time, pending); + } + } + } + + @RequiresApi(api = Build.VERSION_CODES.M) + @Override + public void onReceive(Context context, Intent intent) + { + ArrayList jobs = new ArrayList<>(); + long now = System.currentTimeMillis(); + + synchronized (this) + { + ScheduledJob job = mJobs.peek(); + while (job != null) + { + if (job.Time > now) + { + break; + } + jobs.add(mJobs.remove()); + job = mJobs.peek(); + } + if (job != null) + { + PendingIntent pending = createIntent(); + mManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, job.Time, pending); + } + } + + for (ScheduledJob job : jobs) + { + executeJob(job.Id); + } + } + + /** + * Execute the job with the given ID. + * + * @param id job ID + */ + public native void executeJob(String id); + + /** + * Keep track of scheduled jobs. + */ + private static class ScheduledJob implements Comparable + { + String Id; + long Time; + + ScheduledJob(String id, long time) + { + Id = id; + Time = time; + } + + @Override + public int compareTo(ScheduledJob o) + { + return Long.compare(Time, o.Time); + } + } +} diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk b/src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk index 49e313af5..a42a44e9b 100644 --- a/src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/Android.mk @@ -9,6 +9,7 @@ backend/android_creds.c \ backend/android_fetcher.c \ backend/android_dns_proxy.c \ backend/android_private_key.c \ +backend/android_scheduler.c \ backend/android_service.c \ charonservice.c \ kernel/android_ipsec.c \ diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.c b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.c new file mode 100644 index 000000000..5a901fe15 --- /dev/null +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.c @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2020 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include + +#include "android_scheduler.h" +#include "../android_jni.h" + +#include +#include +#include + +typedef struct private_scheduler_t private_scheduler_t; + +/** + * Private data. + */ +struct private_scheduler_t { + + /** + * Public interface. + */ + scheduler_t public; + + /** + * Reference to Scheduler object. + */ + jobject obj; + + /** + * Java class for Scheduler. + */ + jclass cls; + + /** + * Hashtable that stores scheduled jobs (entry_t*). + */ + hashtable_t *jobs; + + /** + * Mutex to safely access the scheduled jobs. + */ + mutex_t *mutex; +}; + +/** + * Data for scheduled jobs. + */ +typedef struct { + + /** + * Random identifier as string. + */ + char *id; + + /** + * The scheduled job. + */ + job_t *job; + +} entry_t; + +CALLBACK(destroy_entry, void, + entry_t *this, const void *key) +{ + DESTROY_IF(this->job); + free(this->id); + free(this); +} + +JNI_METHOD(Scheduler, executeJob, void, + jstring jid) +{ + private_scheduler_t *sched; + entry_t *entry; + char *id; + + sched = (private_scheduler_t*)lib->scheduler; + id = androidjni_convert_jstring(env, jid); + sched->mutex->lock(sched->mutex); + entry = sched->jobs->remove(sched->jobs, id); + sched->mutex->unlock(sched->mutex); + free(id); + + if (entry) + { + lib->processor->queue_job(lib->processor, entry->job); + entry->job = NULL; + destroy_entry(entry, NULL); + } +} + +METHOD(scheduler_t, get_job_load, u_int, + private_scheduler_t *this) +{ + u_int count; + + this->mutex->lock(this->mutex); + count = this->jobs->get_count(this->jobs); + this->mutex->unlock(this->mutex); + return count; +} + +/** + * Allocate an ID for a new job. We do this via JNI so we don't have to rely + * on RNGs being available when we replace the default scheduler. + */ +static jstring allocate_id(private_scheduler_t *this, JNIEnv *env) +{ + jmethodID method_id; + + method_id = (*env)->GetMethodID(env, this->cls, "allocateId", + "()Ljava/lang/String;"); + if (!method_id) + { + return NULL; + } + return (*env)->CallObjectMethod(env, this->obj, method_id); +} + +METHOD(scheduler_t, schedule_job_ms, void, + private_scheduler_t *this, job_t *job, uint32_t ms) +{ + JNIEnv *env; + jmethodID method_id; + entry_t *entry = NULL; + jstring jid; + + androidjni_attach_thread(&env); + jid = allocate_id(this, env); + if (!jid) + { + goto failed; + } + method_id = (*env)->GetMethodID(env, this->cls, "scheduleJob", + "(Ljava/lang/String;J)V"); + if (!method_id) + { + goto failed; + } + + this->mutex->lock(this->mutex); + INIT(entry, + .id = androidjni_convert_jstring(env, jid), + .job = job, + ); + job->status = JOB_STATUS_QUEUED; + this->jobs->put(this->jobs, entry->id, entry); + this->mutex->unlock(this->mutex); + + (*env)->CallVoidMethod(env, this->obj, method_id, jid, (jlong)ms); + if (androidjni_exception_occurred(env)) + { + goto failed; + } + androidjni_detach_thread(); + return; + +failed: + DBG1(DBG_JOB, "unable to schedule job for execution in %u ms", ms); + if (entry) + { + this->mutex->lock(this->mutex); + this->jobs->remove(this->jobs, entry->id); + this->mutex->unlock(this->mutex); + destroy_entry(entry, NULL); + } + else + { + job->destroy(job); + } + androidjni_exception_occurred(env); + androidjni_detach_thread(); +} + +METHOD(scheduler_t, schedule_job_tv, void, + private_scheduler_t *this, job_t *job, timeval_t tv) +{ + timeval_t now; + + time_monotonic(&now); + + if (!timercmp(&now, &tv, <)) + { + /* already expired, just send it to the processor */ + lib->processor->queue_job(lib->processor, job); + return; + } + timersub(&tv, &now, &now); + schedule_job_ms(this, job, now.tv_sec * 1000 + now.tv_usec / 1000); +} + +METHOD(scheduler_t, schedule_job, void, + private_scheduler_t *this, job_t *job, uint32_t s) +{ + schedule_job_ms(this, job, s * 1000); +} + +METHOD(scheduler_t, flush, void, + private_scheduler_t *this) +{ + JNIEnv *env; + jmethodID method_id; + + this->mutex->lock(this->mutex); + this->jobs->destroy_function(this->jobs, destroy_entry); + this->jobs = hashtable_create(hashtable_hash_str, hashtable_equals_str, 16); + this->mutex->unlock(this->mutex); + + androidjni_attach_thread(&env); + method_id = (*env)->GetMethodID(env, this->cls, "Terminate", "()V"); + if (!method_id) + { + androidjni_exception_occurred(env); + } + else + { + (*env)->CallVoidMethod(env, this->obj, method_id); + androidjni_exception_occurred(env); + } + androidjni_detach_thread(); +} + +METHOD(scheduler_t, destroy, void, + private_scheduler_t *this) +{ + JNIEnv *env; + + androidjni_attach_thread(&env); + if (this->obj) + { + (*env)->DeleteGlobalRef(env, this->obj); + } + if (this->cls) + { + (*env)->DeleteGlobalRef(env, this->cls); + } + androidjni_detach_thread(); + this->mutex->destroy(this->mutex); + this->jobs->destroy(this->jobs); + free(this); +} + +/* + * Described in header + */ +scheduler_t *android_scheduler_create(jobject context) +{ + private_scheduler_t *this; + JNIEnv *env; + jmethodID method_id; + jobject obj; + jclass cls; + + INIT(this, + .public = { + .get_job_load = _get_job_load, + .schedule_job = _schedule_job, + .schedule_job_ms = _schedule_job_ms, + .schedule_job_tv = _schedule_job_tv, + .flush = _flush, + .destroy = _destroy, + }, + .jobs = hashtable_create(hashtable_hash_str, hashtable_equals_str, 16), + .mutex = mutex_create(MUTEX_TYPE_DEFAULT), + ); + + androidjni_attach_thread(&env); + cls = (*env)->FindClass(env, JNI_PACKAGE_STRING "/Scheduler"); + if (!cls) + { + goto failed; + } + this->cls = (*env)->NewGlobalRef(env, cls); + method_id = (*env)->GetMethodID(env, cls, "", + "(Landroid/content/Context;)V"); + if (!method_id) + { + goto failed; + } + obj = (*env)->NewObject(env, cls, method_id, context); + if (!obj) + { + goto failed; + } + this->obj = (*env)->NewGlobalRef(env, obj); + androidjni_detach_thread(); + return &this->public; + +failed: + DBG1(DBG_JOB, "failed to create Scheduler object"); + androidjni_exception_occurred(env); + androidjni_detach_thread(); + destroy(this); + return NULL; +} diff --git a/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.h b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.h new file mode 100644 index 000000000..a27491084 --- /dev/null +++ b/src/frontends/android/app/src/main/jni/libandroidbridge/backend/android_scheduler.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Tobias Brunner + * HSR Hochschule fuer Technik Rapperswil + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. See . + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +/** + * @defgroup android_scheduler android_scheduler + * @{ @ingroup android_backend + */ + +#ifndef ANDROID_SCHEDULER_H_ +#define ANDROID_SCHEDULER_H_ + +#include + +#include + +/** + * Create an Android-specific scheduler_t implementation. + * + * @param context Context object + * @return scheduler_t instance + */ +scheduler_t *android_scheduler_create(jobject context); + +#endif /** ANDROID_SCHEDULER_H_ @}*/