android: Add Android-specific implementation of scheduler_t

This uses AlarmManager to schedule events in a way that ensures the app
is woken up (requires whitelisting when in Doze mode to be woken up at
the exact time, otherwise there are delays of up to 15 minutes).
This commit is contained in:
Tobias Brunner 2020-04-30 18:55:23 +02:00
parent aaa908dc0a
commit b7d66ae2cd
4 changed files with 513 additions and 0 deletions

View File

@ -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 <http://www.fsf.org/copyleft/gpl.txt>.
*
* 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<ScheduledJob> 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<ScheduledJob> 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<ScheduledJob>
{
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);
}
}
}

View File

@ -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 \

View File

@ -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 <http://www.fsf.org/copyleft/gpl.txt>. *
* 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 <sys/time.h>
#include "android_scheduler.h"
#include "../android_jni.h"
#include <collections/hashtable.h>
#include <processing/jobs/callback_job.h>
#include <threading/mutex.h>
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, "<init>",
"(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;
}

View File

@ -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 <http://www.fsf.org/copyleft/gpl.txt>.
*
* 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 <jni.h>
#include <processing/scheduler.h>
/**
* 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_ @}*/