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,