diff --git a/python-for-android/dists/kolibri/build.gradle b/python-for-android/dists/kolibri/build.gradle
index f557d1f4..cae61c05 100644
--- a/python-for-android/dists/kolibri/build.gradle
+++ b/python-for-android/dists/kolibri/build.gradle
@@ -108,5 +108,6 @@ dependencies {
implementation 'androidx.concurrent:concurrent-futures:1.1.0'
implementation 'androidx.work:work-runtime:2.9.0'
implementation 'androidx.work:work-multiprocess:2.9.0'
+ implementation "androidx.lifecycle:lifecycle-service:2.7.0"
implementation 'net.sourceforge.streamsupport:java9-concurrent-backport:2.0.5'
}
diff --git a/python-for-android/dists/kolibri/src/main/AndroidManifest.xml b/python-for-android/dists/kolibri/src/main/AndroidManifest.xml
index e1a9694f..a77a41ee 100644
--- a/python-for-android/dists/kolibri/src/main/AndroidManifest.xml
+++ b/python-for-android/dists/kolibri/src/main/AndroidManifest.xml
@@ -28,6 +28,7 @@
+
-
-
localInstance = new ThreadLocal<>();
+ private final Context context;
+
+ public PythonProvider(Context context) {
+ this.context = context;
+ localInstance.set(this);
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+
+ public void close() {
+ localInstance.remove();
+ }
+
+ public static PythonProvider create(Context context) {
+ if (isActive()) {
+ throw new RuntimeException("PythonProviders cannot be nested");
+ }
+ return new PythonProvider(context);
+ }
+
+ public static PythonProvider get() {
+ if (!isActive()) {
+ throw new RuntimeException("PythonProvider not initialized");
+ }
+ return PythonProvider.localInstance.get();
+ }
+
+ public static boolean isActive() {
+ return localInstance.get() != null;
+ }
+}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/kivy/android/PythonWorker.java b/python-for-android/dists/kolibri/src/main/java/org/kivy/android/PythonWorker.java
index 9ea02666..381c027b 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/kivy/android/PythonWorker.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/kivy/android/PythonWorker.java
@@ -6,8 +6,6 @@
import androidx.annotation.NonNull;
-import java.io.File;
-
/**
* Ideally this would be called `PythonWorkerImpl` but the name is used in the native code.
*/
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/ContextUtil.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/ContextUtil.java
index 037ff58c..d47f5ce8 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/ContextUtil.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/ContextUtil.java
@@ -3,12 +3,12 @@
import android.content.Context;
import org.kivy.android.PythonActivity;
-import org.learningequality.Kolibri.WorkerService;
+import org.kivy.android.PythonProvider;
public class ContextUtil {
public static Context getApplicationContext() {
- if (isServiceContext()) {
- return WorkerService.mService.getApplicationContext();
+ if (PythonProvider.isActive()) {
+ return PythonProvider.get().getContext();
}
if (isActivityContext()) {
return PythonActivity.mActivity.getApplicationContext();
@@ -19,8 +19,4 @@ public static Context getApplicationContext() {
public static boolean isActivityContext() {
return PythonActivity.mActivity != null;
}
-
- public static boolean isServiceContext() {
- return WorkerService.mService != null;
- }
}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/BackgroundWorker.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/BackgroundWorker.java
index ffc2897d..aa9801b6 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/BackgroundWorker.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/BackgroundWorker.java
@@ -7,42 +7,27 @@
import androidx.annotation.NonNull;
import androidx.work.WorkerParameters;
+import org.learningequality.notification.Manager;
+import org.learningequality.notification.NotificationRef;
import org.learningequality.task.Worker;
-import org.kivy.android.PythonWorker;
+
+import org.learningequality.Kolibri.task.TaskWorkerImpl;
/**
* Background worker that runs a Python task in a background thread. This will likely be run by the
* SystemJobService.
*/
-final public class BackgroundWorker extends androidx.work.Worker implements Worker {
+final public class BackgroundWorker extends Worker {
private static final String TAG = "Kolibri.BackgroundWorker";
- private final PythonWorker workerImpl;
public BackgroundWorker(
@NonNull Context context, @NonNull WorkerParameters workerParams
) {
super(context, workerParams);
- workerImpl = new PythonWorker(context, "TaskWorker", "taskworker.py");
- }
-
- /**
- * Parent worker class will call this method on a background thread automatically.
- */
- @Override
- @NonNull
- public Result doWork() {
- Log.d(TAG, "Running background task " + getId());
- final String id = getId().toString();
- final String arg = getArgument();
- Result r = workerImpl.execute(id, arg) ? Result.success() : Result.failure();
- hideNotification();
- return r;
}
- @Override
- public void onStopped() {
- Log.d(TAG, "Stopping background remote task " + getId());
- super.onStopped();
- hideNotification();
+ protected TaskWorkerImpl getWorkerImpl() {
+ Log.d(TAG, "Starting background task: " + getId());
+ return new TaskWorkerImpl(getId(), getApplicationContext());
}
}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/ForegroundWorker.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/ForegroundWorker.java
index a7c2c3f5..003a80b4 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/ForegroundWorker.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/ForegroundWorker.java
@@ -1,102 +1,60 @@
package org.learningequality.Kolibri;
-import android.annotation.SuppressLint;
+import android.app.Notification;
import android.content.pm.ServiceInfo;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.concurrent.futures.CallbackToFutureAdapter;
import androidx.work.ForegroundInfo;
-import androidx.work.impl.utils.futures.SettableFuture;
-import androidx.work.multiprocess.RemoteListenableWorker;
import com.google.common.util.concurrent.ListenableFuture;
+import org.learningequality.Kolibri.task.TaskWorkerImpl;
import org.learningequality.notification.Builder;
import org.learningequality.notification.NotificationRef;
import org.learningequality.task.Worker;
-import org.kivy.android.PythonWorker;
-import java.util.concurrent.Future;
-import java.util.concurrent.ThreadPoolExecutor;
-
-final public class ForegroundWorker extends RemoteListenableWorker implements Worker {
+final public class ForegroundWorker extends Worker {
private static final String TAG = "Kolibri.ForegroundWorker";
- private final PythonWorker workerImpl;
public ForegroundWorker(
@NonNull android.content.Context context,
@NonNull androidx.work.WorkerParameters workerParams
) {
super(context, workerParams);
- workerImpl = new PythonWorker(context, "TaskWorker", "taskworker.py");
}
- @SuppressLint("RestrictedApi")
- @Override
- @NonNull
- public ListenableFuture startRemoteWork() {
- Log.d(TAG, "Running foreground remote task " + getId());
- final SettableFuture future = SettableFuture.create();
- final String id = getId().toString();
- final String arg = getArgument();
-
- // See executor defined in configuration
- final ThreadPoolExecutor executor = (ThreadPoolExecutor) getBackgroundExecutor();
- // This is somewhat similar to what the plain `Worker` class does, except that we
- // use `submit` instead of `execute` so we can propagate cancellation
- // See https://android.googlesource.com/platform/frameworks/support/+/60ae0eec2a32396c22ad92502cde952c80d514a0/work/workmanager/src/main/java/androidx/work/Worker.java
- final Future> threadFuture = executor.submit(() -> {
- try {
- Result r = workerImpl.execute(id, arg) ? Result.success() : Result.failure();
- future.set(r);
- } catch (Exception e) {
- Log.e(TAG, "Exception in remote python work for " + id, e);
- future.setException(e);
- }
- });
-
- // If `RunnableFuture` was a `ListenableFuture` we could simply use `future.setFuture` to
- // propagate the result and cancellation, but instead add listener to propagate
- // cancellation to python thread, using the task executor which should invoke this in the
- // main thread (where this was originally called from)
- future.addListener(() -> {
- synchronized (future) {
- if (future.isCancelled()) {
- Log.i(TAG, "Interrupting python thread");
- synchronized (threadFuture) {
- threadFuture.cancel(true);
- }
- }
-
- if (future.isDone()) {
- hideNotification();
- }
- }
- }, getTaskExecutor().getMainThreadExecutor());
- return future;
+ protected TaskWorkerImpl getWorkerImpl() {
+ Log.d(TAG, "Starting foreground task: " + getId());
+ return new TaskWorkerImpl(getId(), getApplicationContext());
}
@Override
- public void onStopped() {
- Log.d(TAG, "Stopping foreground remote task " + getId());
- super.onStopped();
- hideNotification();
+ @NonNull
+ public Result doWork() {
+ Log.d(TAG, "Setting task as foreground: " + getId());
+ setForegroundAsync(getForegroundInfo());
+ return super.doWork();
}
+ @NonNull
public ForegroundInfo getForegroundInfo() {
- NotificationRef ref = WorkerService.buildNotificationRef();
- Builder builder = new Builder(getApplicationContext(), ref);
+ NotificationRef ref = getNotificationRef();
+ Notification lastNotification = this.getLastNotification();
+ if (lastNotification == null) {
+ // build default notification
+ lastNotification = new Builder(getApplicationContext(), ref).build();
+ }
// If API level is at least 29
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
return new ForegroundInfo(
ref.getId(),
- builder.build(),
+ lastNotification,
ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST
);
}
-
- return new ForegroundInfo(ref.getId(), builder.build());
+ return new ForegroundInfo(ref.getId(), lastNotification);
}
@Override
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/WorkerService.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/WorkerService.java
deleted file mode 100644
index 68a4bb5a..00000000
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/WorkerService.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package org.learningequality.Kolibri;
-
-import android.content.Intent;
-import android.os.IBinder;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.work.multiprocess.RemoteWorkerService;
-
-import org.learningequality.notification.NotificationRef;
-import org.learningequality.notification.Notifier;
-
-/**
- * Dedicated service for running tasks in the foreground via RemoteListenableWorker.
- */
-public class WorkerService extends RemoteWorkerService implements Notifier {
- private static final String TAG = "Kolibri.ForegroundWorkerService";
-
- public static WorkerService mService = null;
-
- @Override
- public void onCreate() {
- Log.d(TAG, "Initializing foreground worker service");
- super.onCreate();
- mService = this;
- }
-
- @Override
- public void onDestroy() {
- Log.d(TAG, "Destroying foreground worker service");
- hideNotification();
- super.onDestroy();
- mService = null;
- }
-
- public NotificationRef getNotificationRef() {
- return buildNotificationRef();
- }
-
- public static NotificationRef buildNotificationRef() {
- return new NotificationRef(NotificationRef.REF_CHANNEL_SERVICE);
- }
-}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/Builder.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/Builder.java
index 3547ff5c..5c84a166 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/Builder.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/Builder.java
@@ -9,11 +9,9 @@
import androidx.work.OutOfQuotaPolicy;
import androidx.work.WorkInfo;
import androidx.work.WorkQuery;
-import androidx.work.multiprocess.RemoteListenableWorker;
import org.learningequality.Kolibri.BackgroundWorker;
import org.learningequality.Kolibri.ForegroundWorker;
-import org.learningequality.Kolibri.WorkerService;
import org.learningequality.Kolibri.sqlite.JobStorage;
import org.learningequality.task.Worker;
@@ -161,16 +159,6 @@ private Data buildInputData() {
String dataArgument = id == null ? "" : id;
Data.Builder builder = new Data.Builder()
.putString(Worker.ARGUMENT_WORKER_ARGUMENT, dataArgument);
-
- if (longRunning || expedite) {
- builder.putString(
- RemoteListenableWorker.ARGUMENT_PACKAGE_NAME, "org.learningequality.Kolibri"
- )
- .putString(
- RemoteListenableWorker.ARGUMENT_CLASS_NAME,
- WorkerService.class.getName()
- );
- }
Data data = builder.build();
Log.v(TAG, "Worker request data: " + data.toString());
return data;
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/TaskWorkerImpl.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/TaskWorkerImpl.java
new file mode 100644
index 00000000..21f9da85
--- /dev/null
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/Kolibri/task/TaskWorkerImpl.java
@@ -0,0 +1,105 @@
+package org.learningequality.Kolibri.task;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.work.Data;
+
+import org.kivy.android.PythonWorker;
+
+import org.learningequality.task.Observer;
+import org.learningequality.task.WorkerImpl;
+
+import java.util.List;
+import java.util.UUID;
+
+public class TaskWorkerImpl extends PythonWorker implements WorkerImpl {
+ private static final ThreadLocal localInstance = new ThreadLocal<>();
+ private final UUID id;
+ private final List> observers;
+
+ public TaskWorkerImpl(UUID id, @NonNull Context context) {
+ super(context, "TaskWorker", "taskworker.py");
+ this.id = id;
+ observers = new java.util.ArrayList<>();
+ localInstance.set(this);
+ }
+
+ public void addObserver(Observer observer) {
+ observers.add(observer);
+ }
+ public void removeObserver(Observer observer) {
+ observers.remove(observer);
+ }
+
+ public void notifyObservers(@Nullable Message message) {
+ if (message == null) {
+ return;
+ }
+ for (Observer observer : observers) {
+ observer.update(message);
+ }
+ }
+
+ public void close() {
+ observers.clear();
+ localInstance.remove();
+ }
+
+ protected Message buildMessage(
+ String notificationTitle, String notificationText, int progress, int total
+ ) {
+ return new Message(notificationTitle, notificationText, progress, total);
+ }
+
+ /**
+ * This method is called by the python side, when progress is updated
+ */
+ public static void notifyLocalObservers(
+ String notificationTitle, String notificationText, int progress, int total
+ ) {
+ TaskWorkerImpl instance = localInstance.get();
+ if (instance != null) {
+ instance.notifyObservers(
+ instance.buildMessage(notificationTitle, notificationText, progress, total)
+ );
+ }
+ }
+
+ public class Message {
+ public static final String KEY_ID = "id";
+ public static final String KEY_NOTIFICATION_TITLE = "notificationTitle";
+ public static final String KEY_NOTIFICATION_TEXT = "notificationText";
+ public static final String KEY_PROGRESS = "progress";
+ public static final String KEY_TOTAL_PROGRESS = "totalProgress";
+
+ public final String notificationTitle;
+ public final String notificationText;
+ public final int progress;
+ public final int totalProgress;
+
+ public Message(
+ String notificationTitle, String notificationText, int progress, int totalProgress
+ ) {
+ this.notificationTitle = notificationTitle;
+ this.notificationText = notificationText;
+ this.progress = progress;
+ this.totalProgress = totalProgress;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public Data toData() {
+ return new Data.Builder()
+ .putString(KEY_ID, id.toString())
+ .putString(KEY_NOTIFICATION_TITLE, notificationTitle)
+ .putString(KEY_NOTIFICATION_TEXT, notificationText)
+ .putInt(KEY_PROGRESS, progress)
+ .putInt(KEY_TOTAL_PROGRESS, totalProgress)
+ .build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Builder.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Builder.java
index 3bcf81ca..8fb0409d 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Builder.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Builder.java
@@ -52,7 +52,7 @@ public Builder(Context context, String channelId) {
}
public Builder(Context context, int channelRef) {
- this(context, NotificationRef.getChannelId(context, channelRef));
+ this(context, NotificationRef.getChannelId(channelRef));
}
public Builder(Context context, NotificationRef ref) {
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Manager.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Manager.java
index c5866d8c..9270e450 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Manager.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Manager.java
@@ -1,7 +1,11 @@
package org.learningequality.notification;
+import android.Manifest;
+import android.app.Notification;
import android.content.Context;
+import android.content.pm.PackageManager;
+import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationManagerCompat;
public class Manager {
@@ -17,9 +21,9 @@ public void send() {
send(null, null, -1, -1);
}
- public void send(String notificationTitle, String notificationText, int notificationProgress, int notificationTotal) {
+ public Notification prepare(String notificationTitle, String notificationText, int notificationProgress, int notificationTotal) {
if (ref == null) {
- return;
+ return null;
}
Builder builder = new Builder(context, ref);
if (notificationTitle != null) {
@@ -31,8 +35,21 @@ public void send(String notificationTitle, String notificationText, int notifica
if (notificationProgress != -1 && notificationTotal != -1) {
builder.setProgress(notificationTotal, notificationProgress, false);
}
+ return builder.build();
+ }
+
+ public Notification send(String notificationTitle, String notificationText, int notificationProgress, int notificationTotal) {
+ if (ref == null) {
+ return null;
+ }
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
+ // TODO: handle this case
+ return null;
+ }
+ Notification notification = prepare(notificationTitle, notificationText, notificationProgress, notificationTotal);
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
- notificationManager.notify(ref.getTag(), ref.getId(), builder.build());
+ notificationManager.notify(ref.getTag(), ref.getId(), notification);
+ return notification;
}
public void hide() {
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/NotificationRef.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/NotificationRef.java
index 08aa1242..a1cc400b 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/NotificationRef.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/NotificationRef.java
@@ -1,8 +1,5 @@
package org.learningequality.notification;
-import android.content.Context;
-
-import org.learningequality.Kolibri.R;
public final class NotificationRef {
public static final int ID_DEFAULT = 1;
@@ -24,8 +21,8 @@ public NotificationRef(int channelRef, String tag) {
this(channelRef, ID_DEFAULT, tag);
}
- public NotificationRef(int channelRef) {
- this(channelRef, ID_DEFAULT, null);
+ public NotificationRef(int channelRef, int id) {
+ this(channelRef, id, null);
}
public int getChannelRef() {
@@ -40,7 +37,7 @@ public String getTag() {
return tag;
}
- public static String getChannelId(Context context, int channelRef) {
+ public static String getChannelId(int channelRef) {
switch (channelRef) {
case REF_CHANNEL_SERVICE:
return ID_CHANNEL_SERVICE;
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Notifier.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Notifier.java
index b04ee2f6..9ddc8c9b 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Notifier.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/notification/Notifier.java
@@ -1,5 +1,6 @@
package org.learningequality.notification;
+import android.app.Notification;
import android.content.Context;
@@ -18,13 +19,13 @@ default Manager getNotificationManager(NotificationRef ref) {
return new Manager(getApplicationContext(), ref);
}
- default void sendNotification(
+ default Notification sendNotification(
String notificationTitle,
String notificationText,
int notificationProgress,
int notificationTotal
) {
- getNotificationManager(getNotificationRef())
+ return getNotificationManager(getNotificationRef())
.send(notificationTitle, notificationText, notificationProgress, notificationTotal);
}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observable.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observable.java
new file mode 100644
index 00000000..9a531428
--- /dev/null
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observable.java
@@ -0,0 +1,12 @@
+package org.learningequality.task;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Small interface for an observerable which can be observed for updates with an Observer.
+ */
+public interface Observable {
+ void addObserver(Observer observer);
+ void removeObserver(Observer observer);
+ void notifyObservers(@Nullable T message);
+}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observer.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observer.java
new file mode 100644
index 00000000..d0525a13
--- /dev/null
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Observer.java
@@ -0,0 +1,10 @@
+package org.learningequality.task;
+
+import androidx.annotation.Nullable;
+
+/**
+ * Small interface for an observer that listens for updates from an observable.
+ */
+public interface Observer {
+ void update(@Nullable T message);
+}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Worker.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Worker.java
index 6eb49209..5f7d6dfe 100644
--- a/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Worker.java
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/Worker.java
@@ -1,21 +1,105 @@
package org.learningequality.task;
+import android.app.Notification;
+import android.content.Context;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
import androidx.work.Data;
+import androidx.work.WorkerParameters;
+import org.kivy.android.PythonProvider;
+import org.learningequality.Kolibri.task.TaskWorkerImpl;
import org.learningequality.notification.Notifier;
import org.learningequality.notification.NotificationRef;
import java.util.UUID;
+import java.util.zip.CRC32;
+
+/**
+ * Abstract worker class that executes a task worker implementation
+ */
+abstract public class Worker extends androidx.work.Worker implements Notifier {
+ public static String TAG = "Kolibri.BaseWorker";
+ public static String ARGUMENT_WORKER_ARGUMENT = "PYTHON_WORKER_ARGUMENT";
+ private int lastProgressUpdateHash;
+ private Notification lastNotification;
+
+ public Worker(
+ @NonNull Context context, @NonNull WorkerParameters workerParams
+ ) {
+ super(context, workerParams);
+ }
+
+ /**
+ * Parent worker class will call this method on a background thread automatically
+ * when work is to be executed.
+ */
+ protected abstract WorkerImpl getWorkerImpl();
+
+ /**
+ * Parent worker class will call this method on a background thread automatically.
+ */
+ @Override
+ @NonNull
+ public Result doWork() {
+ final String id = getId().toString();
+ final String arg = getArgument();
+ Result r;
+
+ Log.d(TAG, "Executing task implementation: " + getId());
+ try (WorkerImpl workerImpl = getWorkerImpl()) {
+ workerImpl.addObserver(new Observer() {
+ @Override
+ public void update(TaskWorkerImpl.Message message) {
+ onProgressUpdate(message);
+ }
+ });
+ // Provide context to PythonProvider
+ try (PythonProvider ignored = PythonProvider.create(getApplicationContext())) {
+ r = workerImpl.execute(id, arg) ? Result.success() : Result.failure();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Error executing task implementation: " + getId(), e);
+ r = Result.failure();
+ }
+ hideNotification();
+ return r;
+ }
-public interface Worker extends Notifier {
- String TAG = "Kolibri.TaskWorker";
- String ARGUMENT_WORKER_ARGUMENT = "PYTHON_WORKER_ARGUMENT";
+ @Override
+ public void onStopped() {
+ Log.d(TAG, "Stopping background remote task " + getId());
+ hideNotification();
+ super.onStopped();
+ }
- UUID getId();
+ protected Notification getLastNotification() {
+ return lastNotification;
+ }
- Data getInputData();
+ protected void onProgressUpdate(TaskWorkerImpl.Message message) {
+ Data updateData = message.toData();
+ // Only update progress if it has changed
+ if (updateData.hashCode() == lastProgressUpdateHash) {
+ return;
+ }
+ lastProgressUpdateHash = updateData.hashCode();
+ // Logs the data to debug logging
+ setProgressAsync(updateData);
+ try {
+ lastNotification = sendNotification(
+ message.notificationTitle,
+ message.notificationText,
+ message.progress,
+ message.totalProgress
+ );
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to update task progress for: " + getId(), e);
+ }
+ }
- default String getArgument() {
+ protected String getArgument() {
String dataArg = getInputData().getString(ARGUMENT_WORKER_ARGUMENT);
final String serviceArg;
if (dataArg != null) {
@@ -26,16 +110,20 @@ default String getArgument() {
return serviceArg;
}
- default NotificationRef getNotificationRef() {
+ public NotificationRef getNotificationRef() {
// Use worker request ID as notification tag
return buildNotificationRef(getId());
}
- static NotificationRef buildNotificationRef(UUID id) {
+ public static NotificationRef buildNotificationRef(UUID id) {
return buildNotificationRef(id.toString());
}
- static NotificationRef buildNotificationRef(String id) {
- return new NotificationRef(NotificationRef.REF_CHANNEL_DEFAULT, id);
+ public static NotificationRef buildNotificationRef(String id) {
+ // Use CRC32 to generate a unique, integer, notification ID from the string request ID
+ CRC32 crc = new CRC32();
+ crc.update(id.getBytes());
+ int notificationId = (int) crc.getValue(); // Use lower 32 bits (truncates)
+ return new NotificationRef(NotificationRef.REF_CHANNEL_DEFAULT, notificationId);
}
}
diff --git a/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/WorkerImpl.java b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/WorkerImpl.java
new file mode 100644
index 00000000..612a6b75
--- /dev/null
+++ b/python-for-android/dists/kolibri/src/main/java/org/learningequality/task/WorkerImpl.java
@@ -0,0 +1,9 @@
+package org.learningequality.task;
+
+/**
+ * Interface for defining a worker implementation that can be observed for updates, and handles
+ * execution of a task, and cleanup of resources implementing AutoCloseable.
+ */
+public interface WorkerImpl extends Observable, AutoCloseable {
+ boolean execute(String id, String arg);
+}
diff --git a/src/android_app_plugin/kolibri_plugin.py b/src/android_app_plugin/kolibri_plugin.py
index 4439009f..e5ebedea 100644
--- a/src/android_app_plugin/kolibri_plugin.py
+++ b/src/android_app_plugin/kolibri_plugin.py
@@ -9,6 +9,7 @@
Locale = autoclass("java.util.Locale")
Task = autoclass("org.learningequality.Task")
+TaskWorker = autoclass("org.learningequality.Kolibri.task.TaskWorkerImpl")
PROGRESS_LIMIT = 10000
@@ -73,11 +74,10 @@ def update(self, job, orm_job, state=None, **kwargs):
# avoid passing integers that are too large
# PROGRESS_LIMIT gives sufficient precision for a % progress calculation
if total_progress > PROGRESS_LIMIT:
- progress = progress // total_progress * PROGRESS_LIMIT
+ progress = PROGRESS_LIMIT * progress // total_progress
total_progress = PROGRESS_LIMIT
- Task.updateProgress(
- orm_job.worker_extra,
+ TaskWorker.notifyLocalObservers(
status.title,
status.text,
progress,