+ *
+ * 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) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.kde.kdeconnect.async;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public abstract class BackgroundJob implements Runnable {
+ private static AtomicLong atomicLong = new AtomicLong(0);
+ protected volatile boolean canceled;
+ protected BackgroundJobHandler backgroundJobHandler;
+ private long id;
+
+ protected I requestInfo;
+ private Callback callback;
+
+ public BackgroundJob(I requestInfo, Callback
callback) {
+ this.id = atomicLong.incrementAndGet();
+ this.requestInfo = requestInfo;
+ this.callback = callback;
+ }
+
+ void setBackgroundJobHandler(BackgroundJobHandler handler) {
+ this.backgroundJobHandler = handler;
+ }
+
+ public long getId() { return id; }
+ public I getRequestInfo() { return requestInfo; }
+
+ public void cancel() {
+ canceled = true;
+ backgroundJobHandler.cancelJob(this);
+ }
+
+ public boolean isCancelled() {
+ return canceled;
+ }
+
+ public interface Callback
{
+ void onResult(@NonNull BackgroundJob backgroundJob, @Nullable R result);
+ void onProgress(@NonNull BackgroundJob backgroundJob, @Nullable P result);
+ void onError(@NonNull BackgroundJob backgroundJob, @NonNull Throwable error);
+ }
+
+ protected void reportResult(@Nullable R result) {
+ backgroundJobHandler.runOnUiThread(() -> callback.onResult(this, result));
+ backgroundJobHandler.onFinished(this);
+ }
+
+ protected void reportProgress(@Nullable P progress) {
+ backgroundJobHandler.runOnUiThread(() -> callback.onProgress( this, progress));
+ }
+
+ protected void reportError(@NonNull Throwable error) {
+ backgroundJobHandler.runOnUiThread(() -> callback.onError( this, error));
+ backgroundJobHandler.onFinished(this);
+ }
+}
diff --git a/src/org/kde/kdeconnect/async/BackgroundJobHandler.java b/src/org/kde/kdeconnect/async/BackgroundJobHandler.java
new file mode 100644
--- /dev/null
+++ b/src/org/kde/kdeconnect/async/BackgroundJobHandler.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2018 Erik Duisters
+ *
+ * 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) version 3 or any later version
+ * accepted by the membership of KDE e.V. (or its successor approved
+ * by the membership of KDE e.V.), which shall act as a proxy
+ * defined in Section 14 of version 3 of the license.
+ *
+ * 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.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package org.kde.kdeconnect.async;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.RejectedExecutionException;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class BackgroundJobHandler {
+ private static final String TAG = BackgroundJobHandler.class.getSimpleName();
+
+ private static BackgroundJobHandler instance;
+
+ private static final int CORE_POOL_SIZE = 5;
+ private static final int MAXIMUM_POOL_SIZE = 10;
+ private static final int KEEP_ALIVE = 1;
+
+ private final Map> jobMap = new HashMap<>();
+ private final Object jobMapLock = new Object();
+
+ private final ThreadFactory threadFactory = new ThreadFactory() {
+ private final AtomicInteger threadNum = new AtomicInteger(1);
+
+ public Thread newThread(@NonNull Runnable r) {
+ return new Thread(r, "BGThread #" + threadNum.getAndIncrement());
+ }
+ };
+
+ private final BlockingQueue poolWorkQueue = new LinkedBlockingQueue<>(10);
+
+ private class MyThreadPoolExecutor extends ThreadPoolExecutor {
+ MyThreadPoolExecutor(BlockingQueue workQueue, ThreadFactory threadFactory) {
+ super(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, workQueue, threadFactory);
+ }
+
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+
+ if (!(r instanceof Future)) {
+ return;
+ }
+
+ Future> future = (Future>) r;
+
+ if (t == null) {
+ try {
+ future.get();
+ } catch (CancellationException ce) {
+ Log.d(TAG,"afterExecute got an CancellationException");
+ //Thread.currentThread().interrupt();
+ } catch (ExecutionException ee) {
+ t = ee;
+ } catch (InterruptedException ie) {
+ Log.d(TAG, "afterExecute got an InterruptedException");
+ Thread.currentThread().interrupt(); // ignore/reset
+ }
+ }
+
+ if (t != null) {
+ BackgroundJobHandler.this.handleUncaughtException(future, t);
+ }
+ }
+ }
+
+ private final ThreadPoolExecutor threadPool;
+ private Handler handler;
+
+ private BackgroundJobHandler() {
+ this.handler = new Handler(Looper.getMainLooper());
+ this.threadPool = new MyThreadPoolExecutor(poolWorkQueue, threadFactory);
+ }
+
+ public static BackgroundJobHandler getInstance() {
+ if (instance == null) {
+ synchronized (BackgroundJobHandler.class) {
+ if (instance == null) {
+ instance = new BackgroundJobHandler();
+ }
+ }
+ }
+
+ return instance;
+ }
+
+ //TODO: Call onError instead of returning true of false
+ public boolean runJob(BackgroundJob bgJob) {
+ Future> f;
+
+ bgJob.setBackgroundJobHandler(this);
+
+ try {
+ synchronized (jobMapLock) {
+ f = threadPool.submit(bgJob);
+ jobMap.put(bgJob, f);
+ }
+ } catch (RejectedExecutionException e) {
+ Log.d(TAG,"threadPool.submit rejected a background job: " + e.getMessage());
+
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean isRunning(long jobId) {
+ synchronized (jobMapLock) {
+ for (BackgroundJob job : jobMap.keySet()) {
+ if (job.getId() == jobId) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Nullable
+ public BackgroundJob getJob(long jobId) {
+ synchronized (jobMapLock) {
+ for (BackgroundJob job : jobMap.keySet()) {
+ if (job.getId() == jobId) {
+ return job;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Cancel a job
+ *
+ * @param job The job to cancel
+ * @return true if the job was cancelled successfully, false otherwise
+ */
+ void cancelJob(BackgroundJob job) {
+ synchronized (jobMapLock) {
+ if (jobMap.containsKey(job)) {
+ Future> f = jobMap.get(job);
+
+ if (f.cancel(true)) {
+ threadPool.purge();
+ }
+
+ jobMap.remove(job);
+ }
+ }
+ }
+
+ private void handleUncaughtException(Future> ft, Throwable t) {
+ synchronized (jobMapLock) {
+ for (Map.Entry> pairs : jobMap.entrySet()) {
+ Future> future = pairs.getValue();
+
+ if (future == ft) {
+ pairs.getKey().reportError(t);
+ break;
+ }
+ }
+ }
+ }
+
+ void onFinished(BackgroundJob job) {
+ synchronized (jobMapLock) {
+ jobMap.remove(job);
+ }
+ }
+
+ void runOnUiThread(Runnable runnable) {
+ handler.post(runnable);
+ }
+}